Создание веб-приложений без формИсточник: max404
Эта статья основана на предварительной версии платформы ASP.NET MVC. Данные сведения могут быть изменены.
Код доступен для загрузки по адресу: MVCFramework2008_03.exe (189 KB) Я профессионально занимаюсь разработкой уже около 15 лет, а до этого еще лет 10 писал программы ради удовольствия. Как и большинство программистов моего возраста, я начал с 8-битных машин, а потом перешел на платформу ПК. Переходя от системы к системе, по возрастающей, я писал все подряд, начиная с игрушек и заканчивая программами для управления личными данными и контроля внешнего оборудования.
Эти принципы существуют независимо от используемых технологий. Разделение задач - основной способ снизить сложность проблемы. Если вы на один объект навесите сразу несколько обязанностей, скажем, расчет оставшегося времени работы, форматирование данных, построение графика, то тем самым только затрудните обслуживание приложения. А автоматическое тестирование необходимо потому, что без него невозможно создать качественный код, сохранив при этом душевное равновесие. Особенно если вы обновляете старый проект.
Схема MVC (модель - визуализация - контроллер)К счастью, группа разработки ASP.NET всегда прислушивалась к мнению программистов. Они приступили к созданию новой платформы веб-приложений, в которой останутся всеми любимые веб-формы, но задачи конструирования несколько изменятся:
Новая платформа строится на базе схемы MVC (модель - визуализация - контроллер), отсюда и название - ASP.NET MVC. Схема MVC была придумана еще в 70-х, при разработке Smalltalk. В статье я постараюсь показать, насколько хорошо она соответствует самой природе Интернета. MVC делит пользовательский интерфейс на три разных объекта: контроллер получает входные данные и обрабатывает их, модель содержит логику домена, представление оформляет полученный результат. В контексте веб-приложений входные данные - это HTTP-запрос. Ход обработки запросов показан на рис. 1.
В веб-формах этот процесс выглядит совсем по-другому. Там входные данные отправляются на страницу (в представление). Представление отвечает и за обработку входных данных, и за отображение результата. В MVC эти обязанности разделяются.
Создание контроллераДля начала нужно установить Visual Studio 2008 и платформу MVC. На момент написания статьи ее можно найти в составе CTP-версии расширений для ASP.NET, выпущенной в декабре 2007 года (asp.net/downloads/3.5-extensions). Вам понадобится и сами расширения, и набор инструментов MVC Toolkit - в нем есть несколько очень полезных вспомогательных объектов. Когда вы загрузите и установите CTP, в диалоговом окне "Новый проект" появится еще один тип проекта - веб-приложение ASP.NET MVC.
Мы создадим простейший контроллер - он будет возвращать имя, переданное в составе URL-адреса. Если щелкнуть правой кнопкой мыши папку Controllers и выбрать пункт "Добавить элемент", откроется привычное диалоговое окно, но в нем будет несколько новых составляющих, в частности класс контроллеров MVC и несколько компонентов представлений MVC. Проявив чудеса изобретательности, я назвал новый класс HelloController: using System; using System.Web; using System.Web.Mvc; namespace HelloFromMVC.Controllers { public class HelloController : Controller { [ControllerAction] public void Index() { ... } } } Класс контроллера по размерам гораздо меньше страницы. Нам в действительности нужно только вывести его из класса System.Web.Mvc.Controller и добавить атрибут [ControllerAction] для операций. Операция - это такой метод, который вызывается в ответ на запрос того или иного URL-адреса. Операции отвечают за всю обработку, необходимую для визуализации представления. Напишем простенькую операцию, передающую имя представлению: [ControllerAction] public void HiThere(string id) { ViewData["Name"] = id; RenderView("HiThere"); } Операция получает имя из URL-адреса, используя параметр id (все подробности через пару минут), сохраняет его в коллекции ViewData и выполняет визуализацию представления, которое мы назвали HiThere. namespace HelloFromMVC.Tests { [TestClass] public class HelloControllerFixture { [TestMethod] public void HiThereShouldRenderCorrectView() { TestableHelloController controller = new TestableHelloController(); controller.HiThere("Chris"); Assert.AreEqual("Chris", controller.Name); Assert.AreEqual("HiThere", controller.ViewName); } } class TestableHelloController : HelloController { public string Name; public string ViewName; protected override void RenderView( string viewName, string master, object data) { this.ViewName = viewName; this.Name = (string)ViewData["Name"]; } } } Тут происходит следующее. Тест этот сам по себе очень прост: создается экземпляр контроллеры, вызывается метод, результат работы которого известен заранее, и проверяется правильность визуализации представления. У нас проверка выполняется при помощи тестового подкласса, который выполняет переопределение метода RenderView. Тем самым создание HTML фактически замыкается. Нам нужно знать только, правильные ли данные были переданы в представление и правильно ли оно было отображено. Все прочие особенности представления нас не касаются.
Создание представленияРазумеется, в конечном итоге нам нужно получить какой-то HTML-код. Переходим к созданию представления HiThere. Для этого создаем в папке Views подпапку с именем Hello. По умолчанию контроллер ищет представление в каталоге Views\<префикс_контроллера> (префикс контроллера получаем, убирая из его имени слово Controller). То есть, представления для контроллера HelloController должны лежать в папке Views\Hello. Теперь наше решение выглядит так, как показано на рис. 4.
HTML-код представления выглядит так: <html > <head runat="server"> <title>Hi There!</title> </head> <body> <div> <h1>Hello, <%= ViewData["Name"] %></h1> </div> </body> </html> Вам, наверное, бросилось в глаза несколько моментов. Нет тегов с атрибутом runat="server". Нет тегов формы. Элементы управления не объявляются. Все это больше похоже на классический ASP, чем на ASP.NET. Представления MVC отвечают только за порождение выходных данных. Обработчики событий и сложные элементы управления (как в веб-формах) им попросту ни к чему. using System; using System.Web; using System.Web.Mvc; namespace HelloFromMVC.Views.Hello { public partial class HiThere : ViewPage { } } В нем нет ни методов Init и load, ни обработчиков событий - только объявление базового класса (кстати, базовым является не класс Page, а класс ViewPage). Для представления MVC больше ничего и не нужно. Запустите приложение, откройте страницу http://localhost:<порт>/Hello/HiThere/Chris - и вы увидите примерно то, что показано на рис. 5.
Если вместо этой картинки у вас на экране появилось сообщение об исключении, паниковать не стоит. Если файл HiThere.aspx в Visual Studio определен как активный документ, нажмите F5 - Visual Studio попытается получить доступ к файлу напрямую. Поскольку в MVC контроллер должен запускаться заранее, до отображения выходных данных, просто перейти к странице не поможет. Измените URL-адрес так, чтобы он совпадал с тем, что показано на рис. 5, и все должно заработать. public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { // Change Url= to Url="[controller].mvc/[action]/[id]" // to enable automatic support on IIS6 RouteTable.Routes.Add(new Route { Url = "[controller]/[action]/[id]", Defaults = new { action = "Index", id = (string)null }, RouteHandler = typeof(MvcRouteHandler) }); RouteTable.Routes.Add(new Route { Url = "Default.aspx", Defaults = new { controller = "Home", action = "Index", id = (string)null }, RouteHandler = typeof(MvcRouteHandler) }); } } Url = "[controller]/[action]/[id]" Именно этот объект и обеспечивает вызов метода HiThere. Помните, как выглядел URL-адрес? http://localhost/Hello/HiThere/Chris. Объект Route определил, что Hello - это контроллер, HiThere - операция, Chris - ID. Затем платформа MVC создала экземпляр контроллера HelloController, вызывала метод HiThere и присвоила параметру ID значение Chris. RouteTable.Routes.Add(new Route { Url = "[id]", Defaults = new { controller = "Hello", action = "HiThere" }, RouteHandler = typeof(MvcRouteHandler) }); Теперь, стоит открыть страницу http://localhost/Chris, - и на экране появляется мое любимое приветствие.
Пример побольшеТеперь, когда мы рассмотрели основные принципы работы в платформе MVC, я хотел бы привести более масштабный пример. Вики-узел - это веб-узел, содержимое которого можно редактировать в окне обозревателя. Страницы бес труда добавляются к нему и редактируются. Я создал небольшой вики-узел при помощи платформы MVC. Экран редактирования страницы показан на рис. 7.
Логику созданного узла можно посмотреть в коде, приложенном к данной статье. А я хотел бы остановиться на том, каким образом платформа MVC упрощает размещение вики-узла в Интернете. Я начал с проектирования структуры URL-адреса. Он должен был выглядеть так:
Для начала просто отобразим вики-страницу. Для этого я создал класс WikiPageController. Затем я добавил операцию ShowPage. первая версия контроллера WikiPageController показана на рис. 8. Медот ShowPage абсолютно прост. Классы WikiSpace и WikiPage представляют, соответственно, набор вики-страниц и отдельную вики-страницу (и ее редакцию). Созданная операция загружает модель и вызывает метод RenderView. Зачем тогда нужна строка "new WikiPageViewData"? public class WikiPageController : Controller { ISpaceRepository repository; public ISpaceRepository Repository { get { if (repository == null) { repository = new FileBasedSpaceRepository( Request.MapPath("~/WikiPages")); } return repository; } set { repository = value; } } [ControllerAction] public void ShowPage(string pageName, int? version) { WikiSpace space = new WikiSpace(Repository); WikiPage page = space.GetPage(pageName); RenderView("showpage", new WikiPageViewData { Name = pageName, Page = page, Version = version ?? 0 }); } } Один из способов передачи данных из контроллера в представление был продемонстрирован в предыдущем примере: это словать ViewData. Словари - вещь удобная, но коварная. В них может храниться все что угодно, а технология IntelliSense® к содержимому не применяется. Словарь ViewData относится к типу Dictionary<строка, объект>, и поэтому приходится все его содержимое преобразовывать перед использованием. public class WikiPageViewData { public string Name { get; set; } public WikiPage Page { get; set; } public int Version { get; set; } public WikiPageViewData() { Version = 0; } public string NewVersionUrl { get { return string.Format("/CreateNewVersion/{0}", Name); } } public string Body { get { return Page.Versions[Version].Body; } } public string HtmlBody { get { return Page.Versions[Version].BodyAsHtml(); } } public string Creator { get { return Page.Versions[Version].Creator; } } public string Tags { get { return string.Join(",", Page.Versions[Version].Tags); } } } Итак, данные для представления определены. Как их использовать? На странице ShowPage.aspx.cs есть вот такой фрагмент кода: namespace MiniWiki.Views.WikiPage { public partial class ShowPage : ViewPage<WikiPageViewData> { } } Обратите внимание, что базовый класс в нашем примере относится к типу ViewPage<WikiPageViewData>. Это означает, что свойство ViewData, имеющееся у страницы, относится к типу WikiPageViewData, а не к типу Dictionary, как в предыдущем случае. <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs" Inherits="MiniWiki.Views.WikiPage.ShowPage" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContentPlaceHolder" runat="server"> <h1><%= ViewData.Name %></h1> <div id="content" class="wikiContent"> <%= ViewData.HtmlBody %> </div> </asp:Content> Обратите внимание, что в ссылках на ViewData оператор индексации [] не используется. Объект ViewData строго типизирован, поэтому к свойству можно обращаться напрямую. Никаких преобразований при этом не происходит, так что можно воспользоваться технологией IntelliSense. namespace MiniWiki.Views.Layouts { public partial class Site : System.Web.Mvc.ViewMasterPage<WikiPageViewData> { } } Соответствующая разметка показана на рис. 10. В таком виде главная страница используется те же самые объекты ViewData, что и представление. Базовый класс главной страницы в нашем примере имеет тип ViewMasterPage<WikiPageViewData>, значит тип объекта ViewData выбран правильно. Теперь можно добавить теги DIV, чтобы настроить внешний вид страницы, заполнить список версий и, наконец, вставить заполнитель для содержимого. <%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="MiniWiki.Views.Layouts.Site" %> <%@ Import Namespace="MiniWiki.Controllers" %> <%@ Import Namespace="MiniWiki.DomainModel" %> <%@ Import Namespace="System.Web.Mvc" %> <html > <head runat="server"> <title><%= ViewData.Name %></title> <link href="http://../../Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="inner"> <div id="top"> <div id="header"> <h1><%= ViewData.Name %></h1> </div> <div id="menu"> <ul> <li><a href="http://Home">Home</a></li> <li> <%= Html.ActionLink("Edit this page", new { controller = "WikiPage", action = "EditPage", pageName = ViewData.Name })%> </ul> </div> </div> <div id="main"> <div id="revisions"> Revision history: <ul> <% int i = 0; foreach (WikiPageVersion version in ViewData.Page.Versions) { %> <li> <a href="http://<%= ViewData.Name %>?version=<%= i %>"> <%= version.CreatedOn %> by <%= version.Creator %> </a> </li> <% ++i; } %> </ul> </div> <div id="maincontent"> <asp:ContentPlaceHolder ID="MainContentPlaceHolder" runat="server"> </asp:ContentPlaceHolder> </div> </div> </div> </body> </html> Еще следует коснуться вызова Html.ActionLink. Это пример вспомогательного класса визуализации. У классов представлений есть два свойства, Html и Url. Каждый из них использует различные методы для порождения фрагментов HTML-кода. В данном случает Html.ActionLink берет объект (здесь - анонимного типа) и проводит его через систему маршрутизации. В результате получается URL-адрес, по которому определяется контроллер и операция. При таком развитии событий ссыка "Редактировать эту страницу" всегда верна - независимо от того, как изменить маршруты. Создание форм и выполнение обратной передачи [ControllerAction] public void EditPage(string pageName) { WikiSpace space = new WikiSpace(Repository); WikiPage page = space.GetPage(pageName); RenderView("editpage", new WikiPageViewData { Name = pageName, Page = page }); } Опять же, операция эта довольно проста: она просто отображает представление на указанной странице. С представлением, показанном на рис. 11, ситуация чуть интереснее. Это файл создает HTML-форму, но тегов с атрибутом Runat="server" в нем нет. Здесь применяется вспомогательный класс Url.Action - он создает URL-адрес, по которому форма выпоняется обратную передачу. Кроме того, несколько раз используются другие вспомогательные классы: TextBox, TextArea, SubmitButton. Ничего неожиданного они не делают, создают HTML-код для различных полей ввода данных. <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="EditPage.aspx.cs" Inherits="MiniWiki.Views.WikiPage.EditPage" %> <%@ Import Namespace="System.Web.Mvc" %> <%@ Import Namespace="MiniWiki.Controllers" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContentPlaceHolder" runat="server"> <form action="<%= Url.Action( new { controller = "WikiPage", action = "NewVersion", pageName = ViewData.Name })%>" method=post> <% if (ViewContext.TempData.ContainsKey("errors")) { %> <div id="errorlist"> <ul> <% foreach (string error in (string[])ViewContext.TempData["errors"]) { %> <li><%= error%></li> <% } %> </ul> </div> <% } %> Your name: <%= Html.TextBox("Creator", ViewContext.TempData.ContainsKey("creator") ? (string)ViewContext.TempData["creator"] : ViewData.Creator)%> <br /> Please enter your updates here:<br /> <%= Html.TextArea("Body", ViewContext.TempData.ContainsKey("body") ? (string)ViewContext.TempData["body"] : ViewData.Body, 30, 65)%> <br /> Tags: <%= Html.TextBox( "Tags", ViewContext.TempData.ContainsKey("tags") ? (string)ViewContext.TempData["tags"] : ViewData.Tags)%> <br /> <%= Html.SubmitButton("SubmitAction", "OK")%> <%= Html.SubmitButton("SubmitAction", "Cancel")%> </form> </asp:Content> Одна из самых неприятных проблем веб-программирования - ошибки в формах. Точнее говоря, отображение сообщения об ошибке с сохранением введенных данных. Каждому, наверное, случалось допустить ошибку в форме из 35 полей. После этого видишь кучу сообщений об ошибках - и все приходится заполнять заново. Платформа MVC позволяет сохранять введенные данные в элементе TempData. В случае ошибки они вставляются в форму. Элемент ViewState на самом деле здорово упрощает процедуру повторного заполнения веб-форм, поскольку содержимое элементов управления сохраняется по большей части автоматически. [ControllerAction] public void NewVersion(string pageName) { NewVersionPostData postData = new NewVersionPostData(); postData.UpdateFrom(Request.Form); if (postData.SubmitAction == "OK") { if (postData.Errors.Length == 0) { WikiSpace space = new WikiSpace(Repository); WikiPage page = space.GetPage(pageName); WikiPageVersion newVersion = new WikiPageVersion( postData.Body, postData.Creator, postData.TagList); page.Add(newVersion); } else { TempData["creator"] = postData.Creator; TempData["body"] = postData.Body; TempData["tags"] = postData.Tags; TempData["errors"] = postData.Errors; RedirectToAction(new { controller = "WikiPage", action = "EditPage", pageName = pageName }); return; } } RedirectToAction(new { controller = "WikiPage", action = "ShowPage", pageName = pageName }); } Сначала создается объект NewVersionPostData. Это еще один вспомогательный объект, в свойствах и методах которого хранится содержимое передачи и частично выполняется проверка. Для загрузки объекта postData я использую вспомогательную функцию из набора MVC Toolkit. UpdateFrom - это метод расширения, использующий для сопоставления имен полей формы и имен свойств объекта метод отображения. В результате все значения полей загружаются в объект postData. Недостаток метода UpdateFrom в том, что данные из формы он получает при помощи запроса HttpRequest, а это усложняет тестирование модулей.
Создание контроллераБазовая версия вики-узла работает, но есть несколько мест, которые хотелось бы пригладить. Например, свойство Repository используется для отделения логики вики-узла от физического хранилища. Содержимое может храниться в файловой системе (как в моем примере), в базе данных или где-то еще. Здесь возникают две проблемы. public class WikiPageControllerFactory : IControllerFactory { public IController CreateController(RequestContext context, Type controllerType) { return new WikiPageController( GetConfiguredRepository(context.HttpContext.Request)); } private ISpaceRepository GetConfiguredRepository(IHttpRequest request) { return new FileBasedSpaceRepository(request.MapPath("~/WikiPages")); } } В этом случае решение проблемы тривиально, а полученные интерфейс позволяет создавать гораздо более функциональные контроллеры (например, контейнеры для введения зависимостей). Так или иначе, теперь у нас все данные, связанные с получением зависимости, существуют в отдельном объекте, которым без труда можно управлять. ControllerBuilder.Current.SetControllerFactory( typeof(WikiPageController), typeof(WiliPageControllerFactory)); Теперь фабрика зарегистрирована для контроллера WikiPageController. Если бы в проекте были другие контроллеры, они бы не смогли использовать эту фабрику, поскольку харегистрирована она только для WikiPageController. Если фабрику будут использовать все контроллеры, можно вызвать метод SetDefaultControllerFactory.
Другие точки расширенияФабрика контроллеров - лишь одна из возможностей расширения платформы. Все описать в статье не получится, поэтому я коснусь только самых примечательных. Во-первых, если в выходных данных должен содержаться не только HTML-код (или если вы не хотите использовать другой механизм создания шаблонов вместо веб-форм), можно заменить фабрику ViewFactory для контроллера. Интерфейс IViewFactory дает полный контроль над процессом порождения выходных данных. Его можно применять для создания RSS, XML и даже графики.
InvokeAction - это метод, способный определить нужную операцию и вызвать ее. Он допускает настройку этой процедуры (например, можно избавиться от атрибута [ControllerAction]).
Прощайте, веб-формы?Наверное, вы сейчас задаетесь вопросом: "А что же станется с веб-формами? Неужели MVC их полностью заменит?" Нет, конечно. Веб-формы - это хорошо проработанная технология, и Майкрософт будет по-прежнему поддерживать и совершенствовать ее. Если немало приложений, в которых веб-формы прекрасно работают. К примеру, если при создании приложения, выдающего отчеты по базам данных, использовать веб-формы, то на него уйдет гораздо меньше времени, чем при работе в платформе MVC. Кроме того, веб-формы поддерживают широчайший набор элементов управления, большинство которых имеют сложную структуру и потребовали бы немалых усилий, возьмись кто создавать их заново. Крис Таварес (Chris Tavares) работает в группе Microsoft patterns & practices, занимается разработкой рекомендаций по созданию систем на базе платформ Майкрософт. Кроме этого, Крис является виртуальным членом рабочей группы ASP.NET MVC, участвует в работе над новой платформой. Связаться с Крисом можно по адресу cct@tavaresstudios.com. |