Главная страница   /   15.3. Создание выходных данных (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

15.3. Создание выходных данных

После того как контроллер завершит обработку запроса, он обычно должен сгенерировать ответ. Когда мы создавали наш пустой контроллер напрямую путем реализации интерфейса IController, мы должны были взять на себя ответственность за все аспекты обработки запроса, в том числе за создание ответа клиенту. Если мы хотим, например, отправить HTML ответ, то мы должны создать и скомпоновать HTML данные и отправить их клиенту, используя метод Response.Write. Аналогичным образом, если мы хотим перенаправить браузер пользователя на другой URL, мы должны вызвать метод Response.Redirect и передать интересующий нас URL напрямую. Оба этих подхода показаны в листинге 15-7, который демонстрирует улучшения в классе BasicController.

Листинг 15-7: Генерирование результатов в реализации IController
using System.Web.Mvc;
using System.Web.Routing;
namespace ControllersAndActions.Controllers
{
	public class BasicController : IController
	{
		public void Execute(RequestContext requestContext)
		{
			string controller = (string)requestContext.RouteData.Values["controller"];
			string action = (string)requestContext.RouteData.Values["action"];
			if (action.ToLower() == "redirect")
			{
				requestContext.HttpContext.Response.Redirect("/Derived/Index");
			}
			else
			{
				requestContext.HttpContext.Response.Write(
					string.Format("Controller: {0}, Action: {1}",
						controller, action));
			}
		}
	}
}

Вы можете использовать тот же подход, если вы унаследовали ваш контроллер от класса Controller. Класс HttpResponseBase, который возвращается, когда вы читаете свойство requestContext.HttpContext.Response в методе Execute, доступен через свойство Controller.Response, как показано в листинге 15-8, который демонстрирует улучшения в классе DerivedController.

Листинг 15-8: Генерирование выходных данных
using System.Web.Mvc;
namespace ControllersAndActions.Controllers
{
	public class DerivedController : Controller
	{
		public ActionResult Index()
		{
			ViewBag.Message = "Hello from the DerivedController Index method";
			return View("MyView");
		}
		public void ProduceOutput()
		{
			if (Server.MachineName == "TINY")
			{
				Response.Redirect("/Basic/Index");
			}
			else
			{
				Response.Write("Controller: Derived, Action: ProduceOutput");
			}
		}
	}
}

Метод ProduceOutput использует значение свойства Server.MachineName для определения того, какой ответ отправить клиенту. (TINY – это имя одного из наших компьютеров).

Такой подход работает, но тут возникает несколько проблем:

  • Классы контроллеров должны содержать информацию об HTML или URL структуре, и поэтому эти классы труднее читать и поддерживать.
  • Трудно провести модульное тестирование контроллера, который генерирует свой ответ непосредственно в выходные данные. Вам нужно создать mock-реализацию объекта Response, а затем быть в состоянии обработать выходные данные, полученные от контроллера, для того, чтобы определить, что представляет собой результат. Это может означать, например, разбор HTML по ключевым словам, что является длительным и болезненным процессом.
  • Работа с мелкими деталями каждого ответа подобным образом является утомительной и может привести ко многим ошибкам. Некоторым программистам нравится абсолютный контроль, который дает создание «сырых» контроллеров, но нормальные люди довольно быстро разочаровываются в этом.

К счастью, MVC фреймворк предоставляет хорошую функциональную возможность, которая способствует решению всех этих вопросов, и она называется результаты действий. В следующих разделах введено понятие результата действия и показаны различные способы, которыми может быть использован результат действия для получения ответов от контроллеров.

Что такое результаты действия

MVC фреймворк использует результаты действия, чтобы разделить заявление о намерениях от выполнения этих намерений. Она работает очень просто.

Вместо того чтобы работать непосредственно с объектом Response, мы возвращаем объект, наследованный от класса ActionResult, который описывает то, какой ответ от контроллера мы хотели бы увидеть, например, отображение представления или перенаправление на другой URL или метод действия.

Примечание

Система результатов действия является примером шаблона "Команда" (Command pattern). Этот паттерн описывает сценарии, когда вы храните и передаете объекты, описывающие операции, которые должны быть выполнены. См. http://en.wikipedia.org/wiki/Command_pattern для более подробной информации.

Когда MVC получает от метода действия объект ActionResult, он вызывает метод ExecuteResult, определяемый этим объектом. Реализация результата действия затем работает для вас с объектом Response, создавая выходные данные, которые соответствуют вашим намерениям. Простым примером является класс CustomRedirectResult, показанный в листинге 15-9, который мы определили в новой папке Infrastructure нашего проекта.

Листинг 15-9: Класс CustomRedirectResult
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace ControllersAndActions.Infrastructure
{
	public class CustomRedirectResult : ActionResult
	{
		public string Url { get; set; }
		public override void ExecuteResult(ControllerContext context)
		{
			string fullUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext);
			context.HttpContext.Response.Redirect(fullUrl);
		}
	}
}

Мы взяли за основу работы этого класса способ, которым работает класс System.Web.Mvc.RedirectResult: одно из преимуществ того, что MVC является платформой с открытым исходным кодом, заключается в том, что вы можете сами посмотреть, как все это работает. Наш класс CustomRedirectResult намного проще, чем его MVC эквивалент, но этого достаточно для наших целей на данный момент.

Когда мы создаем экземпляр класса RedirectResult, мы передаем ему URL, на который мы хотим перенаправить пользователя. Метод ExecuteResult, который будет выполняться MVC фреймворком, когда наш метод действия закончит работу, получает объект Response для запроса через объект ControllerContext, который предоставляет фреймворк, и вызовет либо метод RedirectPermanent, либо Redirect, а это именно то, что мы делали вручную в листинге 15-8.

Вы можете увидеть, как мы использовали класс CustomRedirectResult, в листинге 15-10, который показывает, как мы изменили контроллер Derived.

Листинг 15-10: Использование класса CustomRedirectResult в контроллере Derived
using ControllersAndActions.Infrastructure;
using System.Web.Mvc;
namespace ControllersAndActions.Controllers
{
	public class DerivedController : Controller
	{
		public ActionResult Index()
		{
			ViewBag.Message = "Hello from the DerivedController Index method";
			return View("MyView");
		}
		public ActionResult ProduceOutput()
		{
			if (Server.MachineName == "TINY")
			{
				return new CustomRedirectResult { Url = "/Basic/Index" };
			}
			else
			{
				Response.Write("Controller: Derived, Action: ProduceOutput");
				return null;
			}
		}
	}
}

Заметьте, что нам нужно было изменить результат действия метода, чтобы вернуть ActionResult. Мы возвращаем null, если мы не хотим, чтобы MVC что-то делал, когда наш метод действия будет выполнен. Это то же, что мы и делаем, когда не возвращаем экземпляр CustomRedirectResult.

Юнит тестирование контроллеров и действий

Многие части MVC фреймворка призваны облегчить модульное тестирование, и это особенно верно для действий и контроллеров. Есть несколько причин для этой поддержки:

  • Вы можете проверить действия и контроллеры вне веб-сервера. Контекстные объекты доступны через свои базовые классы (например, HttpRequestBase), для которых легко применима mock-технология.
  • Вам не нужно разбирать какой-либо HTML для проверки результата метода действия. Вы можете проверить возвращаемый объект ActionResult, чтобы убедиться, что вы получили ожидаемый результат.
  • Вам не нужно имитировать запросы клиентов. Система связывания данных модели MVC фреймворка позволяет вам писать методы действий, которые получают входные данные в качестве параметров метода. Для проверки метода действия вы просто вызываете метод действия и передаете значения параметров, которые вас интересуют.

Мы покажем вам, как создавать юнит тесты для различных видов результатов по мере продвижения по данной главе.

Не забывайте, что модульное тестирование – это не полная история. Сложные виды поведения в приложении возникают тогда, когда методы действий вызываются последовательно. Модульное тестирование лучше всего сочетается с другими видами тестирования.

Теперь, когда вы увидели, как работает наш пользовательский результат действия, мы можем переключиться на один из предоставляемых MVC, который обладает расширенным набором функций и был тщательно протестирован Microsoft. Листинг 15-11 показывает изменения, которые мы внесли.

Листинг 15-11: Использование встроенного объекта RedirectResult
...
public ActionResult ProduceOutput() {
	return new RedirectResult("/Basic/Index");
}
...

Мы удалили условный оператор из метода действия, а это значит, что если вы запустите приложение и перейдите к методу /Derived/ProduceOutput, ваш браузер будет перенаправлен на URL /Basic/Index.

Чтобы упростить код метода действия, класс Controller включает в себя методы для создания различных видов объектов ActionResult. Так, в качестве примера, мы можем добиться результата из листинга 15-11, вернув результат метода Redirect, как показано в листинге 15-12.

Листинг 15-12: Использование метода контроллера для создания результата действия
...
public ActionResult ProduceOutput() {
	return Redirect("/Basic/Index");
}
...

Нет ничего особенно сложного в системе результатов действий, но это поможет вам в конечном итоге получить более простой, более чистый и более последовательный код, который легче читать и который проще для модульного тестирования.

В случае переадресации, например, вы можете просто проверить, что метод действия возвращает экземпляр RedirectResult и что свойство Url содержит ожидаемую цель.

MVC фреймворк содержит ряд встроенных типов результатов действий, которые показаны в таблице 15-2. Все эти типы являются производными от ActionResult, и многие из них имеют удобные вспомогательные методы в классе Controller. В следующих разделах мы покажем вам, как использовать эти типы результатов.

Таблица 15-2: Встроенные типы ActionResult
Тип Описание Вспомогательные методы
ViewResult Отображает указанный шаблон представления или шаблон по умолчанию View
PartialViewResult Отображает указанный шаблон частичного представления или шаблон по умолчанию PartialView
RedirectToRouteResult Работает с HTTP перенаправлением 301 или 302 на метод действия или конкртеный роут, генерируя URL в соответствии с вашей конфигурацией RedirectToAction, RedirectToActionPermanent, RedirectToRoute, RedirectToRoutePermanent
RedirectResult Работает с HTTP перенаправлением 301 или 302 на конкретный URL Redirect, RedirectPermanent
HttpUnauthorizedResult Устанавливает ответный код HTTP статуса на 401 (что означает "не авторизирован"), который вызывает активный механизм аутентификации (form-аутентификацию или Windows-аутентификацию), чтобы попросить посетителя войти в систему Нет
HttpNotFoundResult Возвращает HTTP ошибку 404—Not found HttpNotFound
HttpStatusCodeResult Возвращает указанный HTTP код Нет
EmptyResult Ничего не делает Нет

Возвращение HTML при отображении представления

Наиболее распространенный вид ответа от метода действия заключается в создании HTML и отправке его браузеру. При использовании системы результатов действий вы делаете это путем создания экземпляра класса ViewResult, который определяет желаемое представление для генерирования HTML, как показано в листинге 15-13.

Листинг 15-13: Указание отображаемого представления при помощи ViewResult
using System.Web.Mvc;
namespace ControllersAndActions.Controllers
{
	public class ExampleController : Controller
	{
		public ViewResult Index()
		{
			return View("Homepage");
		}
	}
}

В этом листинге мы используем вспомогательный метод View для создания экземпляра класса ViewResult, который затем возвращается как результат действия метода.

Примечание

Обратите внимание, что возвращаемым типом метода действия в листинге является ViewResult. Метод компилировался бы и работал так же хорошо, если бы мы указали более общий тип ActionResult. На самом деле некоторые MVC программисты определяют результат каждого метода действия как ActionResult, даже если они знают, что всегда будет возвращаться более конкретный тип. В следующих примерах мы все же будем более усердны, чтобы показать вам, как можно использовать каждый тип результата, но мы, как правило, более спокойно относимся к этому в реальных проектах.

Вы указываете представление, которое нужно отобразить, с помощью параметра метода View. В этом примере мы указали представление Homepage.

Примечание

Мы могли бы создать объект ViewResult явно, при помощи return new ViewResult { ViewName = "Homepage" };. Это вполне приемлемый подход, но мы предпочитаем использовать вспомогательные методы, определенные в классе Controller.

Когда MVC вызывает метод ExecuteResult объекта ViewResult, начинается поиск представления, которое вы указали. Если вы используете в вашем проекте области, то фреймворк будет искать в следующих местах:

  • /Areas/<AreaName>/Views/<ControllerName>/<ViewName>.aspx
  • /Areas/<AreaName>/Views/<ControllerName>/<ViewName>.ascx
  • /Areas/<AreaName>/Views/Shared/<ViewName>.aspx
  • /Areas/<AreaName>/Views/Shared/<ViewName>.ascx
  • /Areas/<AreaName>/Views/<ControllerName>/<ViewName>.cshtml
  • /Areas/<AreaName>/Views/<ControllerName>/<ViewName>.vbhtml
  • /Areas/<AreaName>/Views/Shared/<ViewName>.cshtml
  • /Areas/<AreaName>/Views/Shared/<ViewName>.vbhtml

Вы можете видеть из списка, что фреймворк ищет представления, которые были созданы для движка представления ASPX (расширения файлов .aspx и .ascx), даже если был указан Razor, когда создавался проект. Также фреймворк ищет C# и Visual Basic .NET шаблоны Razor (.cshtml файлы – это C#, а .vbhtml – Visual Basic; синтаксис Razor такой же в этих файлах, но фрагменты кода, как можно понять из названий, написаны на разных языках). MVC проверяет по очереди, существует ли каждый из этих файлов. Как только он находит соответствие, он использует представление, чтобы отобразить результат метода действия.

Если вы не используете области или вы используете области, но ни один из файлов в предыдущем списке не был найден, тогда фреймворк продолжит свой поиск в следующих местах:

  • /Views/<ControllerName>/<ViewName>.aspx
  • /Views/<ControllerName>/<ViewName>.ascx
  • /Views/Shared/<ViewName>.aspx
  • /Views/Shared/<ViewName>.ascx
  • /Views/<ControllerName>/<ViewName>.cshtml
  • /Views/<ControllerName>/<ViewName>.vbhtml
  • /Views/Shared/<ViewName>.cshtml
  • /Views/Shared/<ViewName>.vbhtml

Еще раз, как только MVC фреймворк проверит расположение файлов и найдет нужный файл, то поиск прекратится, и найденное представление будет использоваться для отображения ответа клиенту.

Мы не используем в нашем примере приложения области, поэтому фреймворк будет сразу искать в /Views/Example/Index.aspx. Обратите внимание, что опущена часть Controller имени класса, поэтому создание ViewResult в ExampleController приводит к поиску каталога с именем Example.

Юнит тест: отображение представления

Для проверки представления, которое отображает метод действия, вы можете проверить объект ViewResult, который он возвращает. Возможно, это не совсем тот тест, который вы ожидали увидеть, ведь вы не проходите по всему процессу до проверки сгенерированного HTML, но он достаточно хорош, пока у вас есть уверенность в том, что MVC фреймворк работает должным образом. Мы добавили новый проект Unit Test в Visual Studio и добавили файл ActionTests.cs, чтобы в нем содержались наши методы тестирования.

Первая ситуация для проверки появляется тогда, когда метод действия выбирает конкретное представление:

public ViewResult Index() {
	return View("Homepage");
}

Вы можете определить, какое представление было выбрано, прочитав свойство ViewName объекта ViewResult, как показано в данном тестовом методе.

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ControllersAndActions.Controllers;
using System.Web.Mvc;
namespace ControllersAndActions.Tests
{
	[TestClass]
	public class ActionTests
	{
		[TestMethod]
		public void ViewSelectionTest()
		{
			// Arrange - создание контроллера
			ExampleController target = new ExampleController();
			// Act - вызов метода действия
			ViewResult result = target.Index();
			// Assert - проверка результата
			Assert.AreEqual("Homepage", result.ViewName);
		}
	}
}

Небольшое различие возникает тогда, когда вы тестируете метод действия, который выбирает представление по умолчанию:

public ViewResult Index() {
	return View();
}

В такой ситуации вы должны принять пустую строку ("") для названия представления:

Assert.AreEqual("", result.ViewName);

Последовательность директорий, в которой MVC ищет представление, является еще одним примером соглашения о конфигурации. Вам не нужно регистрировать файлы представлений при помощи фреймворка. Вы просто добавляете их в одно из множества известных мест, и фреймворк их найдет. Мы можем пройти на шаг вперед в этом соглашении, опуская имя представления, которое мы хотим отобразить, когда мы вызываем метод View, как показано в листинге 15-14.

Листинг 15-14: Создание ViewResult без указания представления
using System.Web.Mvc;
using System;
namespace ControllersAndActions.Controllers
{
	public class ExampleController : Controller
	{
		public ViewResult Index()
		{
			return View();
		}
	}
}

Когда мы делаем так, MVC предполагает, что мы хотим отобразить представление, которое имеет такое же имя, что и метод действия. Это означает, что вызов метода View в листинге 15-14 запускает поиск представления Index.

Примечание

Результат этого заключается в том, что ищется представление, которое имеет такое же имя, что и метод действия, но имя представления фактически определяется из значения RouteData.Values["action"], которое мы объяснили как часть системы маршрутизации в главах 13 и 14.

Есть несколько перегруженных версий метода View. Они соответствуют настройкам различных свойств объекта ViewResult, который создается. Например, вы можете переопределить макет, который используется представлением, явно указав альтернативу:

...
public ViewResult Index() {
	return View("Index", "_AlternateLayoutPage");
}
...

Указание представления по его пути

Подход с использованием соглашения по именованиям удобен и прост, но таким образом ограничивается число представлений, которые вы можете отобразить. Если вы хотите отобразить конкретное представление, вы можете сделать это, если явно укажете путь к нему и обойдете фазу поиска. Вот пример:

using System.Web.Mvc;
namespace ControllersAndActions.Controllers
{
	public class ExampleController : Controller
	{
		public ViewResult Index()
		{
			return View("~/Views/Other/Index.cshtml");
		}
	}
}

При указании представления наподобие этого путь должен начинаться с / или ~/ и включать в себя расширение имени файла (например, .cshtml для представлений Razor, содержащих C# код).

Если вы собираетесь использовать эту функцию, мы предлагаем вам сделать маленькую паузу и спросить себя, чего вы пытаетесь достичь. Если вы пытаетесь отобразить представление, которое принадлежит другому контроллеру, то лучше перенаправить пользователя на метод действия в этом контроллере (см. раздел "Перенаправление на метод действия" далее в этой главе). Если вы пытаетесь обойти схему именований, потому что данный способ не подходит тому, как вы организовали ваш проект, то см. главу 18, в которой объясняется, как реализовать пользовательскую последовательность поиска.

Передача данных из метода действия в представление

Нам часто нужно передать данные из метода действия в представление. MVC Framework предлагает несколько различных способов сделать это, о чем мы и поговорим в следующих разделах. В этих разделах мы коснемся темы представлений, которые мы подробно рассмотрим в главе 18. В этой главе мы затронем только тот функционал представлений, который необходим для демонстрации интересующих нас возможностей контроллеров.

Предоставление объекта модели представления

Вы можете отправить объект в представление, передав его в качестве параметра методу View, как показано в листинге 15-15.

Листинг 15-15: Указание объекта модели представления
...
public ViewResult Index() {
	DateTime date = DateTime.Now;
	return View(date);
}
...

Мы передали объект DateTime в качестве модели представления. Мы можем обратиться к объекту в представлении, используя ключевое слово Razor Model, как показано в листинге 15-16.

Листинг 15-16: Доступ к модели представления в Razor представлении
@{
	ViewBag.Title = "Index";
}
<h2>Index</h2>
The day is: @(((DateTime)Model).DayOfWeek)

Представление, показанное в листинге 15-16, известно как нетипизированное или слабо типизированное. Представление ничего не знает об объекте модели представления и обрабатывает его как экземпляр object. Чтобы получить значение свойства DayOfWeek, нам нужно привести объект к экземпляру DateTime. Это работает, но представление получается «грязным». Мы можем изменить это, создавая строго типизированные представления, когда мы говорим представлениям, каким будет тип объекта модели представления, как показано в листинге 15-17.

Листинг 15-17: Строго типизированное представление
@model DateTime
@{
	ViewBag.Title = "Index";
}
<h2>Index</h2>
The day is: @Model.DayOfWeek

Мы указываем тип модели представления с помощью ключевого слова Razor model. Обратите внимание, что мы используем строчную m, когда указываем тип модели, и заглавную M, когда мы читаем значение. Не только это помогает очистить наше представление, также Visual Studio поддерживает IntelliSense для строго типизированных представлений, как показано на рисунке 15-3.

Рисунок 15-3: Поддержка IntelliSense для строго типизированных представлений

Юнит тест: объекты модели представления

Вы можете получить доступ к объекту модели представления, переданного методом действия представлению, при помощи свойства ViewResult.ViewData.Model. Вот простой метод действия:

public ViewResult Index() {
	return View((object)"Hello, World");
}

Этот метод действия передает string в качестве объекта модели представления. Мы привели его к object, так что компилятор не думает, что мы хотим перегруженный вариант View, который указывает имя представления. Мы можем получить доступ к объекту модели представления с помощью свойства ViewData.Model, как показано в данном тестовом методе:

...
[TestMethod]
public void ViewSelectionTest() {
	// Arrange - создание контроллера
	ExampleController target = new ExampleController();
	// Act - вызов метода действия
	ViewResult result = target.Index();
	// Assert - проверка результата
	Assert.AreEqual("Hello, World", result.ViewData.Model);
}
...

Передача данных при помощи ViewBag

Мы представили ViewBag в главе 2. Эта возможность позволяет определять произвольные свойства динамического объекта и дает доступ к ним в представлении. Доступ к динамическому объекту можно получить через свойство Controller.ViewBag, как показано в листинге 15-18.

Листинг 15-18: Использование ViewBag
using System;
using System.Web.Mvc;
namespace ControllersAndActions.Controllers
{
	public class ExampleController : Controller
	{
		public ViewResult Index()
		{
			ViewBag.Message = "Hello";
			ViewBag.Date = DateTime.Now;
			return View();
		}
	}
}

В листинге мы определили свойства Message и Date, просто присвоив им значения. До этого момента таких свойств не существовало, и мы не делали никаких приготовлений для их создания. Чтобы прочитать данные обратно в представлении, мы просто получаем те же свойства, которые мы установили в методе действия, как показано в листинге 15-19.

Листинг 15-19: Чтение данных из ViewBag
@{
	ViewBag.Title = "Index";
}
<h2>Index</h2>
The day is: @ViewBag.Date.DayOfWeek
<p />
The message is: @ViewBag.Message

ViewBag имеет преимущество перед использованием объекта модели представления в том, что таким образом легко отправлять несколько объектов в представление. Если бы мы были ограничены в использовании моделей представления, то мы должны были бы создать новый тип, который имеет членов string и DateTime, чтобы получить тот же результат, что и в листингах 15-18 и 15-19.

При работе с динамическими объектами вы можете ввести в представление любую последовательность вызовов методов и свойств:

The day is: @ViewBag.Date.DayOfWeek.Blah.Blah.Blah

Visual Studio не обеспечивает поддержку IntelliSense для любых динамических объектов, в том числе ViewBag, и такие ошибки, как эта, не будут показаны, пока представление не будет отображено.

Нам нравится гибкость ViewBag, но мы склонны придерживаться строго типизированных представлений. Нет никаких ограничений, которые могли бы остановить нас от использования и моделей представления, и ViewBag в том же представлении. Обе эти возможности могут реализованы рядом друг с другом без проблем.

Юнит тест: ViewBag

Вы можете прочитать значения из ViewBag через свойство ViewResult.ViewBag. Следующий тестовый метод предназначен для метода действия из листинга 15-18:

[TestMethod]
public void ViewSelectionTest() {
	// Arrange - создание контроллера
	ExampleController target = new ExampleController();
	// Act - вызов метода действия
	ActionResult result = target.Index();
	// Assert - проверка результата
	Assert.AreEqual("Hello", result.ViewBag.Message);
}
...

Выполнение перенаправлений

Частый результат метода действия заключается не в том, чтобы произвести какие-либо выходные данные напрямую, а в том, чтобы перенаправить браузер пользователя на другой URL. В большинстве случаев этот URL является еще одним методом действие в приложении, который генерирует выходные данные, которые должны быть показаны пользователю.

Паттерн POST/REDIRECT/GET

Наиболее часто перенаправление используется в методах действия, которые обрабатывают запросы HTTP POST. Как мы упоминали в предыдущей главе, запросы POST используются тогда, когда вы хотите изменить состояние приложения. Если вы просто возвращаете HTML после обработки запроса, вы рискуете, что пользователь будет нажимать кнопку перезагрузки браузера и повторит отправку форму во второй раз, вызывая неожиданные и нежелательные результаты.

Чтобы избежать этой проблемы, вы можете следовать паттерну Post/Redirect/Get. В этом паттерне вы получаете POST запрос, обрабатывает его, а затем перенаправляете браузер так, чтобы GET запрос был сделан браузером для другого URL. GET запросы не должны изменять состояние приложения, так что любая случайная повторная отправка этого запроса не вызовет никаких проблем.

Когда вы выполняете перенаправление, вы отправляете браузеру один из двух HTTP кодов:

  • HTTP код 302, который является временным перенаправлением. Это наиболее часто используемый тип перенаправления и при использовании паттерна Post/Redirect/Get, это именно тот код, который вы хотите отправить.
  • HTTP код 301, который указывает на постоянное перенаправление. Его следует использовать с осторожностью, поскольку он говорит получателю HTTP кода никогда снова не запрашивать оригинальный URL, а использовать новый URL, который включен в код перенаправления. Если вы сомневаетесь, используйте временное перенаправление, то есть, отправляйте код 302.

Перенаправление на URL литерального формата

Самый простой способ перенаправить браузер заключается в вызове метод Redirect, который возвращает экземпляр класса RedirectResult, как показано в листинге 15-20.

Листинг 15-20: Перенаправление на URL литерального формата
using System;
using System.Web.Mvc;
namespace ControllersAndActions.Controllers
{
	public class ExampleController : Controller
	{
		public ViewResult Index()
		{
			ViewBag.Message = "Hello";
			ViewBag.Date = DateTime.Now;
			return View();
		}
		public RedirectResult Redirect()
		{
			return Redirect("/Example/Index");
		}
	}
}

URL, на который вы хотите сделать перенаправление, выражается в виде строки и передается в качестве параметра методу Redirect. Метод Redirect реализует временное перенаправление. Вы можете создать постоянное перенаправление, используя метод RedirectPermanent, как показано в листинге 15-21.

Листинг 15-21: Реализация постоянного перенаправления на литеральный URL
...
public RedirectResult Redirect() {
	return RedirectPermanent("/Example/Index");
}
...

Совет

Если вы хотите, вы можете использовать перегруженную версию метода Redirect. Она принимает параметр bool, который определяет, является ли перенаправление постоянным или нет.

Юнит тест: перенаправление на URL литерального формата

Перенаправление на URL литерального формата легко протестировать. Вы можете прочитать URL и проверить, является перенаправление постоянным или временным, используя свойства Url и Permanent класса RedirectResult. Ниже приведен тестовый метод для перенаправления, показанного в листинге 15-21:

...
[TestMethod]
public void RedirectTest() {
	// Arrange - create the controller
	ExampleController target = new ExampleController();
	// Act - call the action method
	RedirectResult result = target.Redirect();
	// Assert - check the result
	Assert.IsFalse(result.Permanent);
	Assert.AreEqual("/Example/Index", result.Url);
}
...

Перенаправление на URL системы маршрутизации

Если вы перенаправляете пользователя в другую часть вашего приложения, вы должны убедиться, что URL, который вы отправляете, действителен внутри вашей URL схемы, как было описано в предыдущей главе. Проблема с использованием URL литерального формата для перенаправления заключается в том, что любые изменения в своей схеме маршрутизации приводят к тому, что вы должны пройти по всему коду и обновить URL.

В качестве альтернативы вы можете использовать систему маршрутизации, чтобы генерировать правильные URL при помощи метода RedirectToRoute, который создает экземпляр RedirectToRouteResult, как показано в листинге 15-22.

Листинг 15-22: Перенаправление на URL системы маршрутизации
using System;
using System.Web.Mvc;
namespace ControllersAndActions.Controllers
{
	public class ExampleController : Controller
	{
		public ViewResult Index()
		{
			ViewBag.Message = "Hello";
			ViewBag.Date = DateTime.Now;
			return View();
		}
		public RedirectToRouteResult Redirect()
		{
			return RedirectToRoute(new
			{
				controller = "Example",
				action = "Index",
				ID = "MyID"
			});
		}
	}
}

Метод RedirectToRoute работает с временным перенаправлением. Используйте метод RedirectToRoutePermanent для постоянного перенаправления. Оба метода принимают анонимный тип, чьи свойства затем передаются системе маршрутизации для генерации URL. Более подробную информацию об этом процессе см. в главе 14.

Юнит тестирование: роутовые перенаправления

Вот юнит тест, который мы добавили в файл ActionTests.cs. Он тестирует метод действия из листинга 15-22:

...
[TestMethod]
public void RedirectValueTest() {
	// Arrange - create the controller
	ExampleController target = new ExampleController();
	// Act - call the action method
	RedirectToRouteResult result = target.RedirectToRoute();
	// Assert - check the result
	Assert.IsFalse(result.Permanent);
	Assert.AreEqual("Example", result.RouteValues["controller"]);
	Assert.AreEqual("Index", result.RouteValues["action"]);
	Assert.AreEqual("MyID", result.RouteValues["ID"]);
}
...

Перенаправление на метод действия

Вы можете сделать перенаправление на метод действия более изящно, используя метод RedirectToAction. Это всего лишь обертка вокруг метода RedirectToRoute, который позволяет указать значения для метода действия и контроллера без необходимости создания анонимного типа, как показано в листинге 15-23.

Листинг 15-23: Перенаправление при помощи RedirectToAction
...
public RedirectToRouteResult RedirectToRoute() {
	return RedirectToAction("Index");
}
...

Если вы просто указываете метод действия, то предполагается, что вы имеете в виду метод действия в текущем контроллере. Если вы хотите сделать перенаправление на другой контроллер, необходимо указать имя в качестве параметра:

...
public RedirectToRouteResult Redirect() {
	return RedirectToAction("Index", "Basic");
}
...

Есть и другие перегруженные версии, которые можно использовать для добавления дополнительных значений при создании URL. Они выражаются при помощи анонимного типа, который имеет тенденцию скрывать цели метода, но все же может сделать ваш код более удобным для чтения.

Примечание

Значения, которые вы передаете методу действия и контроллеру, не проверяются, прежде чем передаются системе маршрутизации. Вы несете ответственность за то, чтобы цели, которые вы определяете, действительно существовали.

Метод RedirectToAction выполняет временное перенаправление. Для постоянного перенаправления используйте RedirectToActionPermanent.

Сохранение данных во время перенаправления

Перенаправление заставляет браузер отправить совершенно новый HTTP запрос, а это означает, что у вас нет доступа к данным оригинального запроса. Если вы хотите передать данные одного запроса другому, вы можете использовать TempData.

Данные TempData похожи на данные Session, за исключением того, что значения TempData помечаются на удаление, после того как они прочитаны, и они удаляются, когда запрос был обработан. Это идеальное средство для сохранения краткосрочных данных, которые необходимо оставить во время выполнения перенаправления. Вот простой пример в методе действия, который использует метод RedirectToAction:

...
public RedirectToRouteResult RedirectToRoute() {
	TempData["Message"] = "Hello";
	TempData["Date"] = DateTime.Now;
	return RedirectToAction("Index");
}
...

Когда этот метод обрабатывает запрос, он устанавливает значения в коллекции TempData, а затем перенаправляет браузер пользователя на метод действия Index в том же контроллере. Вы можете прочитать значения TempData обратно в целевом методе действия, а затем передать их в представление:

...
public ViewResult Index() {
	ViewBag.Message = TempData["Message"];
	ViewBag.Date = TempData["Date"];
	return View();
}
...

Более прямым подходом является прочтение этих значений в представлении:

@{
	ViewBag.Title = "Index";
}
<h2>Index</h2>
The day is: @(((DateTime)TempData["Date"]).DayOfWeek)
<p />
The message is: @TempData["Message"]

Прочтение этих значений в представлении означает, что вам не нужно использовать ViewBag в методе действия. Тем не менее, необходимо привести результаты TempData к соответствующему типу.

Вы можете получить значения из TempData без маркировки их на удаление с помощью метода Peek:

DateTime time = (DateTime)TempData.Peek("Date");

Можно сохранить значения, которое в противном случае были бы удалены, с помощью метода Keep:

TempData.Keep("Date");

Метод Keep не защищает значение навсегда. Если значение будет прочтено еще раз, оно будет помечено для удаления еще раз. Если вы хотите сохранить данные, чтобы они не были удалены после обработки запроса, используйте данные Session.

Возвращение ошибок и HTTP кодов

Последний из встроенных классов ActionResult, которые мы рассмотрим, может быть использован для отправки клиенту сообщений об ошибках и результирующих HTTP кодов. Большинство приложений не требуют этой функции, потому что MVC автоматически генерирует такие виды результатов. Однако они могут быть полезны, если вам нужен более прямой контроль над ответами, которые отправляются клиенту.

Отправка конкретного результирующего HTTP кода

Вы можете отправить конкретный код HTTP статуса браузеру с помощью класса HttpStatusCodeResult. Для этого не существует вспомогательного метода контроллера, так что вы должны создать экземпляр класса напрямую, как показано в листинге 15-24.

Листинг 15-24: Отправка конкретного кода статуса
using System;
using System.Web.Mvc;
namespace ControllersAndActions.Controllers
{
	public class ExampleController : Controller
	{
		public ViewResult Index()
		{
			ViewBag.Message = "Hello";
			ViewBag.Date = DateTime.Now;
			return View();
		}
		public RedirectToRouteResult Redirect()
		{
			return RedirectToAction("Index", "Basic");
		}
		public HttpStatusCodeResult StatusCode()
		{
			return new HttpStatusCodeResult(404, "URL cannot be serviced");
		}
	}
}

Параметры конструктора для HttpStatusCodeResult являются числовым кодом статуса и дополнительным описательным сообщением. В листинге мы вернули код 404, что означает, что запрашиваемый ресурс не существует.

Отправка результата 404

Мы можем достичь того же результата, что показан в листинге 15-24, с помощью более удобного класса HttpNotFoundResult, который является производным от HttpStatusCodeResult и может быть создан с помощью метода контроллера HttpNotFound, как показано в листинге 15-25.

Листинг 15-25: Создание результата 404
...
public HttpStatusCodeResult StatusCode() {
	return HttpNotFound();
}
...

Отправка результата 401

Другим классом-«оберткой» для конкретного кода HTTP статуса является HttpUnauthorizedResult, который возвращает код 401, используемый для указания того, что запрос является неавторизированным. В листинге 15-26 показан пример.

Листинг 15-26: Генерирование результата 401
...
public HttpStatusCodeResult StatusCode() {
	return new HttpUnauthorizedResult();
}
...

В классе Controller нет вспомогательных методов для создания экземпляров HttpUnauthorizedResult, так что вы должны делать это напрямую. Результат возвращения экземпляра этого класса, как правило, заключается в перенаправлении пользователя на страницу аутентификации, как вы видели в главе 11.

Юнит тест: коды HTTP статуса

Класс HttpStatusCodeResult следует тому паттерну, который используется и для других типов результата, и его статус доступен через набор свойств. В данном случае свойство StatusCode возвращает числовой код HTTP статуса, а свойство StatusDescription возвращает соответствующее описание. Следующий тестовый метод предназначен для метода действия из листинга 15-26:

...
[TestMethod]
public void StatusCodeResultTest() {
	// Arrange - create the controller
	ExampleController target = new ExampleController();
	// Act - call the action method
	HttpStatusCodeResult result = target.StatusCode();
	// Assert - check the result
	Assert.AreEqual(401, result.StatusCode);
}
...