Thursday, December 3, 2015

ActionResult in action ASP.NET MVC 5

Здравствуйте, уважаемые читатели. Сегодня мы продолжим погружение в мир ASP.NET MVC 5. И основной посыл данной статьи – рассмотреть использование класса ActionResult. Задача этого класса заключается в том, чтобы представить результат выполняемого метода. По умолчанию к контроллеру доступны снаружи все методы, помеченные как public. Сегодня мы пробежимся по всех доступных классах, которые унаследованы от ActionResult и которые вы можете использовать в своих приложениях.  На рисунке ниже представлен весь список.
Для того чтобы проверить все на практике, давайте создадим новое ASP.NET Web Application, которое назовем “ActionSample”, как показано на рисунке ниже.
Затем выберем стандартный темплейт “MVC”.
ViewResult
Начнем с самого первого варианта рассмотрения ViewResult. Если мы откроем наш контроллер HomeController, то сможем увидеть следующий список методов: 
public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult About()
    {
        ViewBag.Message = "Your application description page.";

        return View();
    }

    public ActionResult Contact()
    {
        ViewBag.Message = "Your contact page.";

        return View();
    }
}
Эти все методы возвращают ViewResult. Этот класс используется для рендеринга представления, используя интерфейс IView, экземпляр которого возвращается с помощью объекта IViewEngine. Такая гибкость позволяет создавать представления на лету. Более детально описание этого класса взято с хабрахабр.
Уделим особое внимание одному особенному классу, наследованному от ActionResult – ViewResult. Этот класс способен найти и отрендерить соответствующий шаблон представления, передав ему что-либо в коллекции ViewData, сформированном в методе действия. Это и есть то, что называется «движок отображения» («view engine») (класс .NET, реализующий интерфейс IViewEngine).
Стандартный движок – это WebFormViewEngine. Его шаблоны представления являются страницами WebForms (.aspx) (то есть серверные страницы, используюшиеся в традиционной технологии ASP.NET WebForms). Страницы WebForms имеют свой собственный конвейер обработки, начинающийся с компиляции ASPX/ASCX «на лету» и проходящий через серию событий, называющийся жизненным циклом страницы. В отличие от традиционного ASP.NET, в ASP.NET MVC эти страницы должны быть максимально простыми, поскольку, согласно принципам MVC, представления могут отвечать только за генерацию HTML кода. Это значит что, вам не требуется детально понимать жизненный цикл страниц WebForms. С соблюдением принципов разделения ролей приходят простота и удобство сопровождения кода.
Наша задача – научиться все использовать самим, поэтому мы создадим новое представление. Благо, редактор в Visual Studio настолько удобен, что от вас потребуется минимум действий.
Добавим в наш код следующий метод:
public ActionResult Info()
{
    ViewBag.Message = "Your info page.";

    return View();
}
Представление для нашего кода делается очень легко через редактор. Для этого в ставим курсор на любую строчку внутри метода Info и нажимаем правую клавишу мыши. Затем в выпадающем меню выбираем “Add View…”, как показано ниже на рисунке.
У нас будет показан дизайнер, как ниже на рисунке.
Мы можем указать темплейт для выбора, выбрать нужною модель, указать, частичное это представление или полное, связать с нужным дата контекстом и другое. Для себя все оставляем так, как показано на рисунке выше, и нажимаем на кнопку “Add”.  После этого у нас будет создана наше представление, как показано ниже.

@{
    ViewBag.Title = "Info";
}

<h2>Info</h2>


После этого мы запустим наш проект на выполнение и добавим в адресную строку браузера /Info.
PartialView
Теперь поскольку мы рассмотрели использование ViewResult, возложив всю работу на дизайнер Visual Studio, пришло время рассмотреть, как работают PartialView. Генерировать PartialView можно разными способами. Как, например, с клиента, используя вызов метода @Html.Partial, так и с сервера, как сейчас рассмотрим мы.
Давайте для начала создадим новую модель. Для этого в папку Models добавим новый класс InfoModel.
public class InfoModel
{
    [Display(Name = "User Id")]
    public int Id { get; set; }
    [Display(Name = "User name")]
    public string Name { get; set; }
}
Затем рассмотрим второй способ создания представлений не с контроллера. Для этого остановимся на папочке Views\Home и нажмем правой кнопкой мыши.
Затем выбираем в темлейтах “Details”, а в model class ищем только что созданный класс, и ставим галочку что нам нужно создать partial view.
Смотрим на то, что нам сгенерировал темплейт.
@model ActionSample.Models.InfoModel

<div>
    <h4>InfoModel</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Name)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Name)
        </dd>

    </dl>
</div>
<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.Id }) |
    @Html.ActionLink("Back to List", "Index")
</p>
Следующим делом добавим новый метод _InfoResult в наш HomeController.
public PartialViewResult _InfoResult()
{
    var infoModel = new InfoModel()
    {
        Id = 1,
        Name = "Alex"
    };
    return PartialView(infoModel);
}
Теперь осталось добавить вызов этого метода с Info.cshtml.

@{
    ViewBag.Title = "Info";
}

<h2>Info</h2>

@{Html.RenderAction("_InfoResult");}
Запускаем теперь наш пример и смотрим на изменения.
RedirectResult
Это очень простой класс, задачей которого является переадресация посредством Url. Зайдем в наш HomeController и добавим новый метод InfoResultRedirect, который будет нас переадресовывать в google поиск.
public ActionResult InfoResultRedirect()
{
    return Redirect("http://www.google.com");
}
Затем перейдем в последнему нашему созданному частичному представлению _InfoResult.cshtml и добавим туда новую ссылку, которая будет адресовать нас к помощи google.
@model ActionSample.Models.InfoModel

<div>
    <h4>InfoModel</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Name)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Name)
        </dd>

    </dl>
</div>
<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.Id }) |
    @Html.ActionLink("Back to List", "Index") |
    @Html.ActionLink("Back to google", "InfoResultRedirect")
</p>
Запускаем наш пример и смотрим? что ссылка работает.
Нажимаем на “Back to googleи убеждаемся в том, что навигация успешно работает.
RedirectToRouteResult
В отличие от предыдущего класса, задача класса RedirectToRouteResult заключается в том, чтобы сделать навигацию на другое событие, используя для этого таблицу маршрутизации вашего сайта. Как это работает – смотрите ниже. Добавим в наш HomeController новый метод, задачей которого будет переход на страницу контактов.
public ActionResult InfoResultRedirectToContact()
{
    return RedirectToAction("Contact");
}
Допиливаем, как обычно, наше представление _InfoResult.cshtml, а точнее – добавим туда новый линк, который будет называться “Contact page”.
@model ActionSample.Models.InfoModel

<div>
    <h4>InfoModel</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Name)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Name)
        </dd>

    </dl>
</div>
<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.Id }) |
    @Html.ActionLink("Back to List", "Index") |
    @Html.ActionLink("Back to google", "InfoResultRedirect") |
    @Html.ActionLink("Contact page", "InfoResultRedirectToContact")
</p>
Запускаем и проверяем, все ли у нас работает так, как нужно.
ContentResult
Данный класс позволяет вернуть пользовательский тип данных. Например, мы можем вернуть отформатированный кусок текста. По умолчанию в классе Controller есть метод Content который возвращает тип ContentResult. По умолчанию результат возвращается как text/plain (ContentType property). Мы же немного усложним задачу и вернем отформатированный с помощью html текст. Поэтому добавляем в наш HomeController метод, который назовем HelloWorld.
public ActionResult HelloWorld()
{
    return Content("<p>Hello World</p>", "text/html");
}
Запускаем наш пример и смотрим на то, что же у нас получилось.
JsonResult
Позволяет вернуть на клиент данные в формате json. Используется очень часто и я уверен, что вам пригодится не один раз. Как обычно. в класс HomeController добавим новый метод, который будет возвращать результат в json формате.
public JsonResult DetailInfo()
{
    return Json(new { Text = "Hello World" }, JsonRequestBehavior.AllowGet);
}
Если мы запустим наш проект, то мы получим файл в формате json, как показано на рисунке ниже.
Как мы с вами понимаем, это удобно только для тестирования. На практике же полученные данные нужно как-то обрабатывать. Поэтому давайте перейдем в нашу представление Info.cshtml и немного его допилим, чтобы полученный результат как-то использовался.

@{
    ViewBag.Title = "Info";
}

<h2>Info</h2>

@{Html.RenderAction("_InfoResult");}

<div id="result"></div>

<script src="~/Scripts/jquery-1.10.2.js"></script>
<script type="text/javascript">
    $(function () {
        $.get("/Home/DetailInfo", function (data) {
            $("#result").html("<p>" + data.Text + "</p>");
        });

    })
</script>
Для этого нам понадобилось использовать jQuery, но код получился несложным для понимания. Запускаем наш проект и смотрим, что наше слово “Hello World” успешно отображается.
JavaScriptResult
С помощью данного класса вы можете вернуть java script результат на клиент. Вероятность того, что вы когда-либо будете это использовать, очень маленькая, да и лучше избегайте по возможности такого подхода, так как он считается антипаттерном MVC. Все то же самое можно сделать успешно на клиенте. Но так как наша цель – это ознакомление, по старинке правим наш HomeController и добавляем туда новый метод UpdateInfo.
public JavaScriptResult UpdateInfo()
{
    var  script = "$('#java-script-update').html('<p> Updated by Alex</p>');";
    return JavaScript(script);
}
В этом скрипте мы ищем по id контрол, а затем обновляем в него значение. То есть, нам нужно просто в наше представление Info.cshtml добавить блок div с именем “java-script-update” и сделать подгруздку скрипта, например, через ajax запрос. Можно вызов сделать через Razor с помощью метода @Ajax.ActionLink, но я предпочитаю это делать через jQuery. Тем более что в предыдущем примере мы уже использовали jQuery.

@{
    ViewBag.Title = "Info";
}

<h2>Info</h2>

@{Html.RenderAction("_InfoResult");}

<div id="result"></div>
<div id="java-script-update"></div>

<script src="~/Scripts/jquery-1.10.2.js"></script>
<script type="text/javascript">
    $(function () {
        $.get("/Home/DetailInfo", function (data) {
            $("#result").html("<p>" + data.Text + "</p>");
        });

    })

    $.ajax({
        url: "/Home/UpdateInfo"
    });
</script>
Как видите, все как я и говорил. Добавился дин блок div к старому коду, и добавился в самом конце вызов метода через ajax запрос. Запускаем наш пример и смотрим, что у нас появилась надпись “Updated by Alex”.
HttpStatusCodeResult
Позволяет возвратить как результат код http запроса и статус (HTTP response code and description). Используется очень часто, когда нужно вернуть какую-то пользовательскую ошибку. Давайте в наш контроллер добавим новый метод Details, который будет возвращать ошибку 410. Полное описание всех кодов ошибок можно посмотреть здесь Status Code Definitions.
10.4.11 410 Gone
The requested resource is no longer available at the server and no forwarding address is known. This condition is expected to be considered permanent. Clients with link editing capabilities SHOULD delete references to the Request-URI after user approval. If the server does not know, or has no facility to determine, whether or not the condition is permanent, the status code 404 (Not Found) SHOULD be used instead. This response is cacheable unless indicated otherwise.
The 410 response is primarily intended to assist the task of web maintenance by notifying the recipient that the resource is intentionally unavailable and that the server owners desire that remote links to that resource be removed. Such an event is common for limited-time, promotional services and for resources belonging to individuals no longer working at the server's site. It is not necessary to mark all permanently unavailable resources as "gone" or to keep the mark for any length of time -- that is left to the discretion of the server owner.
Давайте посмотрим теперь на саму реализацию.
public ActionResult Details(int id)
{
    return new HttpStatusCodeResult(410);
}
Запускаем наш пример и с браузера вызываем метод Details.
HttpUnauthorizedResult
Данный класс используется повсеместно в том случае, когда у пользователя нет прав на получение той или иной информации. Давайте добавим новый метод Page, который будет возвращать нам данный тип ошибки.
public ActionResult Page(int id)
{
    return new HttpUnauthorizedResult("Access is denied");
}
После того как мы в браузере введем /Page/1, например, мы перейдем на страницу авторизации.
Это у нас происходит, потому что в функции ConfigeAuth класса Startup, которая задает начальные настройки для аутентификации, прописана следующая строка кода:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider
    {
        // Enables the application to validate the security stamp when the user logs in.
        // This is a security feature which is used when you change a password or add an external login to your account. 
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromMinutes(30),
            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    }
});       
В случае если вы не введете корректные данные для пользователя, эта строка вас сразу перенаправит на нужную страницу. Давайте закомментируем эту строку кода и запустим наш пример опять.
Теперь мы уже с вами получили то, чего хотели. Небольшое дополнение к данному action result. Наверное, в 90% случаев вы не будете использовать подход с непосредственным возвратом HttpUnauthorizedResult по той причине, что это все сделано через фильтры. Фильтры  это целая тема отдельной статьи, поэтому я просто покажу пример их использования, и вы наверняка вспомните, что видели нечто подобное у себя.
И пример с методом:
Можно написать свои кастомные реализации. Но суть в том, что вы можете задавать всю необходимую логику через фильтры. Например, мы можем переписать наш метод следующим образом:
[Authorize]
public ActionResult Page(int id)
{
    return null;
}
И также получим ошибку авторизации, но уже с помощью фильтров.
Думаю, что такой подход вам пригодится намного больше.
HttpNotFoundResult
Используется в тех случаях, когда, например, нам нужно уведомить пользователя. что некой страницы не существует. Например, с помощью вашего метода хотят получить информацию о кастомере, но передали идентификатор кастомера, которого у вас нет. В ответ вы можете вернуть данный тип результата, и это будет валидно. Давайте подправим написанный для предыдущего примера код и будем возвращать, что страница не найдена, если идентификатор меньше 10-ти.
public ActionResult Page(int id)
{
    if (id < 10)
        return HttpNotFound();
    return new HttpUnauthorizedResult("Access is denied");
}
Запускаем наш пример, чтобы получить нужный результат.
Вводим значение больше 10-ти – получаем старую ошибку с проблемой авторизации.
FileResult
Представляет собой базовый класс и используется для того, чтобы дать возможность отправить бинарный файл в виде ответа. Представлен реализацией такими классами, как FilePathResult, FileContentResult и FileStreamResult, каждый из которых мы рассмотрим дальше в статье.
FilePathResult
Позволяет отправить в виде ответа содержимое файла. В отличие от остальных классов, о которых речь шла выше, первым параметров в данном классе идет путь к файлу. Для того чтобы проверить. как это все работает, я добавил в папку App_Data картинку с драконом.
Затем в контроллер HomeController добавил метод GetPhoto, который будет возвращать эту саму картинку. Ниже приведена реализация этого метода.
public FileResult GetPhoto()
{
    string fileName = HttpContext.Server.MapPath("~/App_Data/dragon.jpg"); ;
    string contentType = "application/jpeg";

    return File(fileName, contentType);
}
Некоторых может смутить, почему указан метод File, а не возвращается непосредственно FilePathResult. Дело в том, что класс Controller, от которого унаследован наш контроллер HomeController, просто напичкан разными вспомогательными методами. Ниже на фото можно увидеть, что делает метод File.
Как видите, в зависимости от первого параметра, он возвращает наружу необходимый класс. Теперь вы можете запустить ваше приложение и получить по конкретному url - ~/Home/GetPhoto вашу картинку. Но так как мы с вами рассматриваем примеры, которые больше подходят для реалий, нам нужно поправить слегка наш клиент Info.cshtml.

@{
    ViewBag.Title = "Info";
}

<h2>Info</h2>
<div>
    <img src="/Home/GetPhoto" />
</div>
@Html.ActionLink("Download dragon picture", "GetPhoto")

@{Html.RenderAction("_InfoResult");}

<div id="result"></div>
<div id="java-script-update"></div>

<script src="~/Scripts/jquery-1.10.2.js"></script>
<script type="text/javascript">
    $(function () {
        $.get("/Home/DetailInfo", function (data) {
            $("#result").html("<p>" + data.Text + "</p>");
        });

    })

    $.ajax({
        url: "/Home/UpdateInfo"
    });
</script>
Здесь я добавил вывод содержимого в img, а также возможность загрузки в виде ссылки “Download dragon image”. Запускаем наш проект и проверяем, что он работает.
Понятное дело, что лучше сразу так картинке не загружать в img, но как загружать какие-то данные с помощью ajax запроса, мы уже рассмотрели, поэтому переписать все это вам не составит труда.
FileContentResult
Отличается от предыдущего только тем ,что выгружает сразу бинарные данные. Добавим по старинке в наш контроллер метод, который для оригинальности назовем GetPhoto1.
public FileResult GetPhoto1()
{
    string fileName = HttpContext.Server.MapPath("~/App_Data/dragon.jpg"); ;
    string contentType = "application/jpeg";
    var body = System.IO.File.ReadAllBytes(fileName);
    return File(body, contentType);
}
В нашем представлении правок нужно сделать по минимуму. Просто заменить с GetPhoto на GetPhoto1. Запустить и убедиться, что все работает, вы можете самостоятельно в этот раз. Тем более. что результат будет такой же, как и в примере выше. Единственное, что будет отличатся, – так это при попытке выгрузить файл имя ему будет предложено GetPhoto1.
FileStreamResult
Как говорит само название класса, задача его  это возвращать нам stream для работы. Особо не заморачиваясь, добавляем метод GetPhoto2.
public FileResult GetPhoto2()
{
    string fileName = HttpContext.Server.MapPath("~/App_Data/dragon.jpg"); ;
    string contentType = "application/jpeg";
    var stream = new System.IO.FileStream(fileName, System.IO.FileMode.Open);
    return File(stream, contentType);
}
Клиент меняем, как и в предыдущем примере, простой заменой GetPhoto1 на GetPhoto2. Работать будет так же, как и в предыдущих двух примерах.
EmptyResult
Самый простой класс, который является надстройкой на null, когда мы возвращаем null с нашего метода. Ниже приведен пример использования.
public EmptyResult Nothing()
{
    return new EmptyResult();
}
Легко заменяется на такой код:
public ActionResult Nothing()
{
    return null;
}
В общем, запускаем и смотрим. Должна быть просто пустая страница.

Итоги
У нас получилась достаточно продуктивная работа. У вас – потому что вы это дочитали до конца, у меня – потому что я это все осилил дописать. Здесь всего по чуть-чуть и все в одном месте. Возможно, вам это пригодится в работе, мне же это будет что-то вроде шпаргалки. Буду рад ответить на ваши вопросы в комментариях.

Исходники к статье вы можете скачать по ссылке: Actions sample.

No comments:

Post a Comment