Создание веб-приложений без форм

Источник: max404

Эта статья основана на предварительной версии платформы ASP.NET MVC. Данные сведения могут быть изменены.
В статье рассматривается:

  • Схема MVC (модель - визуализация - контроллер).
  • Создание контроллеров и представлений.
  • Создание форм и выполнение обратной передачи.
  • Фабрики контроллеров и другие возможности расширения.

Код доступен для загрузки по адресу: MVCFramework2008_03.exe (189 KB)

Я профессионально занимаюсь разработкой уже около 15 лет, а до этого еще лет 10 писал программы ради удовольствия. Как и большинство программистов моего возраста, я начал с 8-битных машин, а потом перешел на платформу ПК. Переходя от системы к системе, по возрастающей, я писал все подряд, начиная с игрушек и заканчивая программами для управления личными данными и контроля внешнего оборудования.
Надо сказать, вначале мои приложения объединяло одно: они все работали локально, на настольном компьютере. В начале 90-х я услышал о такой штуке как Интернет. Замаячила надежда написать веб-приложение, которое сможет вносить записи в карточку учета рабочего времени - тогда бы мне не пришлось каждый раз ехать в офис.
Эта задача оказалась, увы, неразрешимой. В моей настольно-компьютерной голове просто не укладывалось, как выполнить захват при отсутствии состояний. Добавьте к этому далеко не совершенную отладку, сервер под UNIX, на котором у меня не было доступа к корню, чертовы угловые скобки - в общем, я с позором вернулся к разработке программ для настольных компьютеров и занимался ею еще несколько лет.
От веб-программирования я держался в стороне. Разумеется, это важная сторона разработки, но я не понимал самой модели программирования. Затем появились платформы Microsoft® .NET и ASP.NET. Наконец-то мне в руки попало средство, позволяющее работать с веб-приложениями и при этом использовать практически те же приемы разработки, что применялись в локальных программах. Теперь я смогу создавать окна (страницы), привязывать элементы управления к событиям и не маяться с угловыми скобками (слава режиму конструктора!). Лучше всего было то, что ASP.NET сама решила вопрос с отсутствием состояний: появились состояния представления. Я снова стал счастливым программистом... на какое-то время.
С появлением опыта я стал более разборчивым в программировании. Я выработал для себя определенные принципы, которым следовал при работе над приложениями. Два из них выглядят следующим образом.

  • Разделение задач: не смешивай логику интерфейса с поведением элементов.
  • Автоматическое тестирование модулей: всегда проверяй, действительно ли код делает то, что он должен делать.

Эти принципы существуют независимо от используемых технологий. Разделение задач - основной способ снизить сложность проблемы. Если вы на один объект навесите сразу несколько обязанностей, скажем, расчет оставшегося времени работы, форматирование данных, построение графика, то тем самым только затрудните обслуживание приложения. А автоматическое тестирование необходимо потому, что без него невозможно создать качественный код, сохранив при этом душевное равновесие. Особенно если вы обновляете старый проект.
С веб-формами в ASP.NET поначалу очень просто работать, но вот чтобы соблюсти упомянутые принципы, мне пришлось помучиться. Веб-формы целиком и полностью сконцентрированы на интерфейсе. Основной составляющей является страница. Работа начинается с проектирования интерфейса пользователя. Вы перетаскиваете на страницу элементы управления. Возможность слепить логику приложения из обработчиков событий, размещенных на странице (как в Visual Basic® для приложений Windows®), весьма заманчива.
При этом модульное тестирование страниц выполнить очень сложно. Нельзя провести объект-страницу через весь его жизненный цикл, не задействовав всю платформу ASP.NET. Веб-приложения можно тестировать при помощи http-запросов на сервер или при помощи автоматизации обозревателя, однако такое тестирование весьма ненадежно (стоит изменить хоть один идентификатор элемента управления - и все сломается), его сложно настраивать (серверные компоненты на всех компьютерах приходится настраивать абсолютно одинаково), и, кроме того, оно отнимает много времени.
Когда я перешел к относительно сложным веб-приложениям, все эти абстракции, связанные с веб-формами (элементы управления, состояния представления, жизненный цикл страницы), стали больше мешать, чем помогать. У меня все больше времени уходило на настройку привязки данных (и на создание того безумного количества обработчиков, которое требовалось для правильной настройки). Пришлось задуматься над тем, как бы сократить размер состояния представления и ускорить загрузку страниц. Для работы веб-форм необходимо, чтобы по каждому URL-адресу существовал физический файл, а при создании динамических веб-узлов (вики-узлов, к примеру) это возможно не всегда. Создание собственного элемента WebControl (да еще и работоспособного) - процесс многоуровневый, требующий отменного понимания и жизненного цикла страниц, и принципов работы конструктора Visual Studio®.
С тех пор как я пришел в Майкрософт, у меня появилась возможность поделиться тем, что я узнал про болевые точки платформы .NET и про то, как с ними бороться. Совсем недовно я стал участником проекта Patterns & Practices Web Client Software Factory (codeplex.com/websf). Одной из обязательных составляющих приложений, создаваемых нашей группой, является автоматическое тестирование модулей. Для создания тестируемых веб-форм мы предложили использовать шаблон MVP (модель - визуализация - презентатор).
Если коротко, то в схеме MVP логика не размещается на странице. Вместо этого со страницы выполняется обращение к отдельному объекту, презентатору. Объект-презентатор выполняет все необходимые операции в ответ на действия, производимые с представлением, - обычно используя при этом другие объекты (модель) для доступа к данными, для запуска бизнес-логики и т. д. Проделав все необходимое, презентатор обновляет представление. Такой подход расширяет возможности тестирования, поскольку презентатор изолирован от конвейера ASP.NET. Он взаимодействует с представлением через интерфейс, поэтому его можно тестировать отдельно от страницы.
Схема MVP работает неплохо, однако реализовывать ее иногда приходится обходными путями. Нужен отдельный интерфейс представления, нужно большое количество функций передачи событий. Но для обеспечения тестируемости интерфейса веб-форм это, пожалуй, самый лучший вариент. Усовершенствовать его можно, только изменив рабочую платформу.

Схема MVC (модель - визуализация - контроллер)

К счастью, группа разработки ASP.NET всегда прислушивалась к мнению программистов. Они приступили к созданию новой платформы веб-приложений, в которой останутся всеми любимые веб-формы, но задачи конструирования несколько изменятся:

  • Объединение HTTP и HTML (не надо их скрывать).
  • Тестируемость на всех уровнях.
  • Повсеместная расшияремость.
  • Полный контроль над выходными данными.

Новая платформа строится на базе схемы MVC (модель - визуализация - контроллер), отсюда и название - ASP.NET MVC. Схема MVC была придумана еще в 70-х, при разработке Smalltalk. В статье я постараюсь показать, насколько хорошо она соответствует самой природе Интернета. MVC делит пользовательский интерфейс на три разных объекта: контроллер получает входные данные и обрабатывает их, модель содержит логику домена, представление оформляет полученный результат. В контексте веб-приложений входные данные - это HTTP-запрос. Ход обработки запросов показан на рис. 1.


Figure 1 Ход обработки запроса в схеме MVC

В веб-формах этот процесс выглядит совсем по-другому. Там входные данные отправляются на страницу (в представление). Представление отвечает и за обработку входных данных, и за отображение результата. В MVC эти обязанности разделяются.
Сейчас вы, наверное, задаетесь одним из двух вопросов: либо "Ну отлично, а как это применять?", либо "Зачем мне три объекта, если раньше обходились одним?". Оба вопроса вполне осмысленны, и оба можно пояснить на примере. Мы создадим небольное веб-приложение на базе платформы MVC, чтобы продемонстрироватть ее преимущества.

Создание контроллера

Для начала нужно установить Visual Studio 2008 и платформу MVC. На момент написания статьи ее можно найти в составе CTP-версии расширений для ASP.NET, выпущенной в декабре 2007 года (asp.net/downloads/3.5-extensions). Вам понадобится и сами расширения, и набор инструментов MVC Toolkit - в нем есть несколько очень полезных вспомогательных объектов. Когда вы загрузите и установите CTP, в диалоговом окне "Новый проект" появится еще один тип проекта - веб-приложение ASP.NET MVC.
Этот тип проекта несколько отличается от обычного веб-узла или приложения. Шаблон решения создает для веб-приложения другой набор каталогов (см. рис. 2). В каталоге Controllers, к примеру, содержатся классы контроллеров, в каталоге Views (и во всех его вложенных каталогах) - представления.


Figure 2 Структура проекта 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.
Прежде чем перейти к обсуждению того, как этот метод вызывается и что он из себя представляет, я хотел бы коснуться вопросов тестирования. Помните мое замечание о том, как сложно тестировать классы веб-форм? Так вот контроллеры тестировать гораздо проще. Создавать экземпляры контроллеров (и вызывать операции) можно напрямую, безо всякой дополнительной инфраструктуры. Для этого не нужен ни HTTP-контекст, ни сервер - только инструменты тестирования. К примеру, добавим к нашему классу модульный тест Visual Studio Team System (VSTS) (см. рис. 3).
Figure 3 Controller Unit Test

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.


Figure 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 отвечают только за порождение выходных данных. Обработчики событий и сложные элементы управления (как в веб-формах) им попросту ни к чему.
В платформе MVC по-прежнему используется формат ASPX - этот язык удобен для создания шаблонов. Если хотите, можете даже использовать фоновый код. Правда, по умолчанию файл фонового кода выглядит так:

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.


Figure 5 Успешно созданное представление MVC

Если вместо этой картинки у вас на экране появилось сообщение об исключении, паниковать не стоит. Если файл HiThere.aspx в Visual Studio определен как активный документ, нажмите F5 - Visual Studio попытается получить доступ к файлу напрямую. Поскольку в MVC контроллер должен запускаться заранее, до отображения выходных данных, просто перейти к странице не поможет. Измените URL-адрес так, чтобы он совпадал с тем, что показано на рис. 5, и все должно заработать.
Как платформа MVC определяет, что нужно вызвать операцию? В URL-адресе не было даже расширения файла. Ответ кроется в порядке передачи URL-адреса. Взгляните на содержимое файла global.asax.cs (фрагмент из него приведен на рис. 6). Табилица RouteTable содержит коллекцию объектов Route. Каждый из них описывает определенную форму URL-адреса и порядок ее обработки. По умолчанию в таблицу добавляются два объекта Route. Вот первый-то нам и нужен. В нем говорится, что если URL-адрес состоит из трех частей, то первую часть следует считать именем контроллера, выторую - именем операции, а третью - параметром ID:
Figure 6 Route Table

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.
Стандартный Route умеет многое, но можно создавать и собственные объекты. Допустим, я страдаю гипертрофированной вежливостью и хочу, чтобы мой веб-узел выдавал индивидуальное приветствие, как только посетитель введет свое имя. В начало таблицы маршрутизации я добавляю следующее:

RouteTable.Routes.Add(new Route
  {
    Url = "[id]",
    Defaults = new { 
        controller = "Hello", 
        action = "HiThere" },
    RouteHandler = typeof(MvcRouteHandler)
  });

Теперь, стоит открыть страницу http://localhost/Chris, - и на экране появляется мое любимое приветствие.
Как система определила, какой контроллер и какую операцию нужно использовать? Все дело в параметре Defaults. При помощи анонимных конструкций, появившихся в C# 3.0, создается псевдословарь. Параметр Defaults в объекте Route может содержать любую дополнительную информацию. Платформе MVC могут пригодиться записи, относящиеся к контроллеру и операции. Если ни контроллер, ни операция не определены в URL-адресе, платформа использует имена, указанные в параметре Defaults. Вот почему мой запрос был правильно обработан (несмотря на то, что в URL-адресе имена контроллера и операции я не указал).
Еще хочу обратить ваше внимание вот на что. Помните, новый Route мы добавляли в начало таблицы? Если поместить его в конец таблицы, система выдаст ошибку. Маршрутизация работает по принципу живой очереди. При обработке URL-адресов система маршрутизации просматривает таблицу сверху вниз, и используется первый подходящий маршрут. В нашем случае использовался бы стандартный маршрут [контроллер]/[операция]/[id], поскольку для операции и параметры ID выбраны значения по умолчанию. То есть система попыталась бы найти контроллер с именен ChrisController, а поскольку его не существует, возникла бы ошибка.

Пример побольше

Теперь, когда мы рассмотрели основные принципы работы в платформе MVC, я хотел бы привести более масштабный пример. Вики-узел - это веб-узел, содержимое которого можно редактировать в окне обозревателя. Страницы бес труда добавляются к нему и редактируются. Я создал небольшой вики-узел при помощи платформы MVC. Экран редактирования страницы показан на рис. 7.


Figure 7 Редактирование домашней страницы

Логику созданного узла можно посмотреть в коде, приложенном к данной статье. А я хотел бы остановиться на том, каким образом платформа MVC упрощает размещение вики-узла в Интернете. Я начал с проектирования структуры URL-адреса. Он должен был выглядеть так:

  • /[имя_страницы] - отображение страницы с указанным именем.
  • /[имя_страницы]?version=n - отображение указанной версии страницы (0 = текущая версия, 1 = предыдущая и т. д.).
  • /Edit/[имя страницы] - включение режима редактирования для страницы с указанным именем.

  • /CreateNewVersion/[имя_страницы] - сохранение измененной версии.

Для начала просто отобразим вики-страницу. Для этого я создал класс WikiPageController. Затем я добавил операцию ShowPage. первая версия контроллера WikiPageController показана на рис. 8. Медот ShowPage абсолютно прост. Классы WikiSpace и WikiPage представляют, соответственно, набор вики-страниц и отдельную вики-страницу (и ее редакцию). Созданная операция загружает модель и вызывает метод RenderView. Зачем тогда нужна строка "new WikiPageViewData"?
Figure 8 WikiPageController Implementation of ShowPage

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<строка, объект>, и поэтому приходится все его содержимое преобразовывать перед использованием.
Если заранее известно, какие данные будут нужны в представлении, можно не использовать словарь, а передать строго типизированный объект ViewData. Я создал простейший объект WikiPageViewData (он показан на рис. 9). Этот объект передает в представление информацию о вики-странице и несколько методов, способных, к примеру, найти версию HTML в разметке вики-страницы.
Figure 9 WikiPageViewData Object

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, как в предыдущем случае.
Разметка в файле ASPX на самом деле совершенно понятна:

<%@ 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.
Внимательный читатель, наверное, заметил тег <asp:Content> в коде. Да, главные страницы действительно можно использовать вместе с представлениями MVC. Более того, главная страница сама может быть представлением. Взгляните на фоновый код главной страницы:

namespace MiniWiki.Views.Layouts
{
    public partial class Site :  
        System.Web.Mvc.ViewMasterPage<WikiPageViewData>
    {
    }
}

Соответствующая разметка показана на рис. 10. В таком виде главная страница используется те же самые объекты ViewData, что и представление. Базовый класс главной страницы в нашем примере имеет тип ViewMasterPage<WikiPageViewData>, значит тип объекта ViewData выбран правильно. Теперь можно добавить теги DIV, чтобы настроить внешний вид страницы, заполнить список версий и, наконец, вставить заполнитель для содержимого.
Figure 10 Site.Master

<%@ 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-адрес, по которому определяется контроллер и операция. При таком развитии событий ссыка "Редактировать эту страницу" всегда верна - независимо от того, как изменить маршруты.
Ну и наконец, вы, наверное, обратили внимание на то, что ссылки на предыдущие версии страницы пришлось создавать вручную. Текущая версия системы маршрутизации, увы, не способна порождать URL-адреса, если используются строки запросов. В следующих версиях платформы это должно быть исправлено.

Создание форм и выполнение обратной передачи
Теперь рассмотрим операцию EditPage:

[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-код для различных полей ввода данных.
Figure 11 EditPage.aspx

<%@ 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 на самом деле здорово упрощает процедуру повторного заполнения веб-форм, поскольку содержимое элементов управления сохраняется по большей части автоматически.
Было бы здорово проделать то же самое и в MVC. И тут на сцене появляется TempData - это словарь, во многом напоминающий нетипизированный словарь ViewData. Разница состоит в том, что TempData существует только во время выполнения запроса - потом он удаляется. Чтобы понять, как он используется, взгляните на рис. 12 (на операцию NewVersion).
Figure 12 NewVersion Action

[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, а это усложняет тестирование модулей.
Первое, что проверяет NewVersion - это операция SubmitAction. Проверка считается пройденной, если пользователь, нажавший кнопку OK, действительно пытался опубликовать отредактированную страницу. Если в операции присутствуют какие-то другие значения, идет перенаправление к операции ShowPage и просто отображается исходная страница.
Если пользователь все же нажал кнопку OK, проверяется свойство postData.Errors. Выполняется несколько тестов применительно к содержимому передачи. Если ошибок не обнаружено, новая версия страницы сохраняется на вики-узле. Гораздо интереснее, если ошибки обнаружены.
В таком случает содержимое объекта PostData записывается в словарь TempData. Затем пользователь возвращается на страницу редактирования. Форма заполняется значениями, сохраненными в словаре TempData.
Сейчас вся эта процедура передачи, проверки и записи в словарь TempData довольно неприятна и слишком много всего приходится делать вручную. В следующих версиях платформы должны появиться вспомогательные методы, которые помогут автоматизировать работу, - по крайней мере проверку словаря TempData. Последнее, что нужно про него вказать: содержимое словаря TempData хранится в сеансе работы пользователя с сервером. Если сеанс завершить, TempData работать не будет.

Создание контроллера

Базовая версия вики-узла работает, но есть несколько мест, которые хотелось бы пригладить. Например, свойство Repository используется для отделения логики вики-узла от физического хранилища. Содержимое может храниться в файловой системе (как в моем примере), в базе данных или где-то еще. Здесь возникают две проблемы.
Во-первых, класс контроллера тесно связан с устойчивым классом FileBasedSpaceRepository. Приходится задавать значение по умолчанию, чтобы в случае отсутствия значения у свойства было что использовать. Более того, путь к файлам на диске тоже задается жестко. Он должен по крайней мере определяться конфигурацией.
Во-вторых, свойство Repository по факту является обязательным: без него объект не работает. Хорошо бы, чтобы репозиторий задавался в параметре конструктора, а не в свойстве. Но в конструктор его добавить нельзя, потому что по требованиям платформы MVC на контроллерах должны использоваться конструкторы без параметров.
К счастью, существует обработчик расширений, способный помочь в такой ситуации: фабрика контроллеров. Как следует из ее называния, фабрика контроллеров создает экземпляры объектов Controller. Нужно создать класс, реализующий интерфейс IControllerFactory, и зарегистрировать его в системе MVC. Фабрики можно регистрировать для отдельных типов контроллеров или для всех контроллеров сразу. На рис. 13 показана фабрика, созданная для контроллера WikiPageController, - она позволяет указать репозиторий в параметре конструктора.
Figure 13 Controller Factory

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 добавим в файл Global.asax.cs, в метод Application_Start, следующу строку. Добавить ее можно как до, так и после маршрутов.

ControllerBuilder.Current.SetControllerFactory(
  typeof(WikiPageController), typeof(WiliPageControllerFactory));

Теперь фабрика зарегистрирована для контроллера WikiPageController. Если бы в проекте были другие контроллеры, они бы не смогли использовать эту фабрику, поскольку харегистрирована она только для WikiPageController. Если фабрику будут использовать все контроллеры, можно вызвать метод SetDefaultControllerFactory.

Другие точки расширения

Фабрика контроллеров - лишь одна из возможностей расширения платформы. Все описать в статье не получится, поэтому я коснусь только самых примечательных. Во-первых, если в выходных данных должен содержаться не только HTML-код (или если вы не хотите использовать другой механизм создания шаблонов вместо веб-форм), можно заменить фабрику ViewFactory для контроллера. Интерфейс IViewFactory дает полный контроль над процессом порождения выходных данных. Его можно применять для создания RSS, XML и даже графики.
Система маршрутизации, как уже отмечалось, отличается достаточной гибкостью. В ней нет ни одного компонента, присущего исключтельно платформе MVC. Каждый маршрут имеет свойство RouteHandler. До сих пор оно всегда имело значение MvcRouteHandler. Однако можно использовать также интерфейс IRouteHandler и привязать систему маршрутизации к другим веб-технологиям. В следующей версии платформы появится WebFormsRouteHandler, значит в можно будет пользоваться дополнительными технологиями.
Контроллеры не обязательно выводить из класса System.Web.Mvc.Controller. Единственное требование - каждый контроллер должен реализовывать интерфейс IController, в котором есть только один метод (Execute). А дальше вы можете делать все что угодно. С другой стороны, у базового класса Controller есть ряд виртуальных функций, и если их переопределить, можно настроить его поведение:

  • Функции OnPreAction, OnPostAction и OnError позволяют для любой операции задать параметры обработки, выполняемой перед операцией или после нее. OnError - это механизм обработки ошибок.
  • Функция HandleUnknownAction вызывается, когда URL-адрес передается контроллеру, если контроллер не модет выполнить действие, указанное в маршруте. По умолчанию этот метод выдает исключение, но вы можете его настроить на свое усмотрение.

InvokeAction - это метод, способный определить нужную операцию и вызвать ее. Он допускает настройку этой процедуры (например, можно избавиться от атрибута [ControllerAction]).
В классе Controller есть и другие виртуальные методы, но они скорее применяются для тестирования, а не для расширения возможностей. К примеру, метод RedirectToAction является виртуальным, что позволяет вывести из него класс, который в действительности не будет выполнять перенаправление. Это позволяет протестировать операции перенаправления без применения веб-сервера.

Прощайте, веб-формы?

Наверное, вы сейчас задаетесь вопросом: "А что же станется с веб-формами? Неужели MVC их полностью заменит?" Нет, конечно. Веб-формы - это хорошо проработанная технология, и Майкрософт будет по-прежнему поддерживать и совершенствовать ее. Если немало приложений, в которых веб-формы прекрасно работают. К примеру, если при создании приложения, выдающего отчеты по базам данных, использовать веб-формы, то на него уйдет гораздо меньше времени, чем при работе в платформе MVC. Кроме того, веб-формы поддерживают широчайший набор элементов управления, большинство которых имеют сложную структуру и потребовали бы немалых усилий, возьмись кто создавать их заново.
Когда лучше предпочесть MVC веб-формам? Это во многом зависит от конкретных требований и ваших личных предпочтений. Вам надоело бороться с URL-адресами, добиваясь нужной их формы? Вам нужно средство модульного тестирования интерфейса? И в том, и в другом случае стоит, наверное, обратиться к платформе MVC. Вам приходится отображать большое количество данных, вы используете сложные сетки и деревья? Тогда лучше остановить свой выбор на веб-формах.
Со временем платформа MVC, вероятно, догонит веб-формы по уровню сложности элементов управления, но вряд ли когда-то с ней будет так же просто начать работать, как с веб-формами, в которых большинство действий сводится к перетаскиванию элементов. Но между тем, платформа ASP.NET MVC предлагает веб-разработчикам совершенно новый способ создания приложений в Microsoft .NET Framework. В ней есть широкие возможности тестирования, в ней делается упор на HTTP, она отлично масштабируется. Она станет отличным дополнением к веб-формам в том случае, когда разработчику требуется полный контроль над веб-приложением.

Крис Таварес (Chris Tavares) работает в группе Microsoft patterns & practices, занимается разработкой рекомендаций по созданию систем на базе платформ Майкрософт. Кроме этого, Крис является виртуальным членом рабочей группы ASP.NET MVC, участвует в работе над новой платформой. Связаться с Крисом можно по адресу cct@tavaresstudios.com.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=22978