/
Автор: Сандерсон С.
Теги: компьютерные технологии программирование язык программирования c#
ISBN: 978-5-8459-1609-9
Год: 2010
Текст
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" /> <%=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 г. неожиданно стало весьма удобным. Если приложение обращается к коду посторонней сборки, обычно нет возможности входить в исходный код этой сборки во время отладки (поскольку исходный код просто отсутствует). Однако если поставщик эт