ASP.NET MVC Wizard
Для начала определим, какие шаги будут у Мастера:
- Имя и фамилия
- Предпочитаемый язык программирования
- Подтверждение введенных данных
- Пока информации о завершении
Создаем новый MVC проект, назовем его "MvcWizard1"... когда студия спросит или создавать Unit Test - отвечаем нет.
Ничего удалять не будем... для примера готовая функциональность даже очень подойдет)))
Model
Для начала нужно создать класс, который будет в себе хранить информацию из Мастера (Wizard).
Создаем новый класс в каталоге "Models" и назовем его ProfileData.
[Serializable] // об этом будет идти речь ближе к концу статьи public class ProfileData { public string Name { get; set; } public string Surname { get; set; } public string ProgrammingLanguage { get; set; } }
Controller
Создаем контроллер нашего Мастера. Называем его WizardProfileController. Удалим все что для нас создала студия в классе (метод Index и комент).
Создание методов:
По плану у нас должно быть 4 метода, которые буду управлять состоянием Мастера.
Методы:
- 1 п. PersonalInfo,
- 2 п. ProgrammingInfo
- 3 п. Confirm
- 4 п. Complete
Также добавим в класс переменную, которая будет хранить все введенные данные – переменная типа ProfileData;
public class WizardProfileController : Controller { protected ProfileData _data = new ProfileData(); public ActionResult PersonalInfo() { return View(_data); } public ActionResult ProgrammingInfo() { return View(_data); } public ActionResult Confirm() { return View(_data); } public ViewResult Complete() { return View(_data); } }
Создаем для каждого метода свой View. При создании View отметьте пункт Create a strongly-typed view, и в поле View data class указать MvcWizard1.Models.ProfileData, и жмем Add.
На данном этапе проект должен выглядеть вот так:
Views
Редактирование PersonalInfo.aspx:
Нужно два поля для ввода - имени и фамилии. А также еще одна кнопка Next, для навигации к следующему шагу.
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>PersonalInfo</h2> <% using (Html.BeginForm()) { %> <p>Name: <%= Html.TextBox("Name")%></p> <p>Surname: <%= Html.TextBox("Surname")%></p> <input type="submit" name="nextButton" value="Next >>" /> <% } %> </asp:Content>
Редактирование ProgrammingInfo.aspx:
Нужно поле для ввода предпочитаемого языка программирования, и две кнопки: Next и Back.
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>ProgrammingInfo</h2> <% using (Html.BeginForm()) {%> <p>Preferable Programming Language: <%= Html.TextBox("ProgrammingLanguage")%></p> <input type="submit" name="backButton" value="<< Back" /> <input type="submit" name="nextButton" value="Next >>" /> <% } %> </asp:Content>
Редактирование Confirm.aspx:
Будет показываться сумарная информация, которую пользователь ввел на предыдущих двух шагах, а также будет две кнопки: Back и Confirm.
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>Confirm</h2> <% using (Html.BeginForm()) { %> <p>Name: <b><%= Html.Encode(Model.Name) %></b></p> <p>Surname: <b><%= Html.Encode(Model.Surname) %></b></p> <p>Preferable programming language <b><%= Html.Encode(Model.ProgrammingLanguage) %></b></p> <input type="submit" name="backButton" value="<< Back" /> <input type="submit" name="confirmButton" value="Confirm" /> <% } %> </asp:Content>
WizardProfileController
Чтобы узнать на какую кнопку в View нажал пользователь, нам просто нужно будет добавить текстовый параметр с названием этой кнопки в метод, который принимает ответ от этой вьюшки, и если он будет не null значит, пользователь нажал эту кнопку.
Но все-таки лучше это увидеть в виде работающего кода:
public class WizardProfileController : Controller { protected ProfileData _data = new ProfileData(); public ActionResult PersonalInfo(string nextButton) { if (nextButton != null) return RedirectToAction("ProgrammingInfo"); return View(_data); } public ActionResult ProgrammingInfo(string backButton, string nextButton) { if (backButton != null) return RedirectToAction("PersonalInfo"); else if (nextButton != null) return RedirectToAction("Confirm"); return View(_data); } public ActionResult Confirm(string backButton, string confirmButton) { if (backButton != null) return RedirectToAction("ProgrammingInfo"); else if (confirmButton != null) return RedirectToAction("Complete"); return View(_data); } public ViewResult Complete() { return View(_data); } }
Теперь мы можем ходить между пунктами мастера (steps). Но пока те данные, которые вносятся к формы, уничтожаются после перехода на новую страницу.
Сохранение данных между переходами
В ASP.NET MVC нету ViewState, поэтому нам нужно будет создать что-то похожее.
Мы можем сохрянять данные:
1. на стороне Сервера (Server-side) - Сессия (Session), База данных (Data base) и т.д.;
2. на стороне Клиента (Client-side) – в самой странице, в cookie пользователя и т.д.
Сохранение в БД и куках не такая уж и хорошая идия для нашего случая. Сохранение в сесии (Session) не очень приветствутся, так как это хоть и самый простой способ, но эти данные могут быть потеряны в связи с перегрузкой сайта, или время сессии вышло, или сервер вообще захочет освободить немного места для какихто других целей…
Поэтому будем сохранять данные в самой странице… Для этого используем скрытое поле, которые реализует Html helper – Html.Hidden()… с помощь этого метода, в виде строки мы будем сохранять обьект ProfileData.
Потом перед каждым вызовом любых из черырех методом нашего контроллера мастера (WizardProfileController) мы будем доставать тот объект что сохранили с помощью Html.Hidden() с страницы… обновим этот объект данными которые пользователь ввел перед отправкой… потом передадим управление тому методу, который вызывался (дальше просто методХ)…. и перед возвратом результата с методХ, мы сохроним наш объект ProfileData обратно на страницу, и потом позволим методХ возратить результат.
У контроллера есть методы, которые нам очень помогут в реализации выше сказаного:
Это
1. OnActionExecuting() - перед выполнением логической части метода
2. OnActionExecuted() - после выполениея логической части метода
3. OnResultExecuting() - перед возвращением результата с метода
4. OnResultExecuted() - после возвращения результата с метода
Нам понадобятся только OnActionExecuting() и OnResultExecuting(). Добавим их в наш класс WizardProfileController
protected override void OnActionExecuting(ActionExecutingContext filterContext) { // geting PersonalInfo object from hidden field in view } protected override void OnResultExecuting(ResultExecutingContext filterContext) { // saving PersonInfo object to hidden field in view }
В этом же классе меняем строчку:
protected ProfileData _data = new ProfileData();
на
protected ProfileData _data;
Чтобы передать в ф-цию Html.Hidden() те данные, которые нужно сохранить, сначала их нужно записать в ViewData.
Давайте создадим переменную _dataKey которая будет хранить в себе ключ, по которому мы сохраяем наши дданые.
protected string _dataKey = "_profileData";
и будем сохранять вот так: ViewData[_dataKey] = данные
На данном этапе, у нас вот такой класс WizardProfileController:
public class WizardProfileController : Controller { protected ProfileData _data = new ProfileData(); protected string _dataKey = "_profileData"; public ActionResult PersonalInfo(string nextButton) { if (nextButton != null) return RedirectToAction("ProgrammingInfo"); return View(_data); } public ActionResult ProgrammingInfo(string backButton, string nextButton) { if (backButton != null) return RedirectToAction("PersonalInfo"); else if (nextButton != null) return RedirectToAction("Confirm"); return View(_data); } public ActionResult Confirm(string backButton, string confirmButton) { if (backButton != null) return RedirectToAction("ProgrammingInfo"); else if (confirmButton != null) return RedirectToAction("Complete"); return View(_data); } public ViewResult Complete() { return View(_data); } protected override void OnActionExecuting(ActionExecutingContext filterContext) { // geting PersonalInfo object from hidden field in view } protected override void OnResultExecuting(ResultExecutingContext filterContext) { // saving PersonInfo object to hidden field in view } }
Теперь давайте модернизируем наши Вьюшки (Views), чтобы можно было сохранять ProfileData на странице.
После строчек:
<% using (Html.BeginForm()) { %>
напишем
<%= Html.Hidden("_profileData", ViewData["_profileData"]) %>
в PersonalInfo.aspx, ProgrammingInfo.aspx и Confirm.aspx.
Но так как мы не сможем сохранить просто обьект, нам нужно его както преобразовать в текст, чтобы потом сохранить на странице… об этом дальше…
Сериализация
Чтобы сохранить объект на странице, его нужно преобразовать в текст (сериализовать (serialization)), потом когда будем «доставать» из странице, то нужно преобразовать обратно в объект (десиреализовать (deserializitation)).
Создадим хелпер (helper), который будем нам серализвать/десериализовать.
Создадим новый католог в корне ВебСайта и назовем его “Helpers”. В этом катологе создадим новый класс SerializationHelper, и добавим два метода - Serialize, Deserialize:
public class SerializationHelper { public static string Serialize(object obj) { StringWriter writer = new StringWriter(); (new LosFormatter()).Serialize(writer, obj); return writer.ToString(); } public static object Deserialize(string data) { if (String.IsNullOrEmpty(data)) return null; return (new LosFormatter()).Deserialize(data); } }
Теперь чтобы сериализовать объект (у класса должен быть параметр [Serializable] как у ProfileData) и передать его в метод Serizlize(). Чтобы десериализовать обьект, передать строку сериализованого объекта в метод Deserialize().
Обратно к WizardProfileController
public class WizardProfileController : Controller { // все что выше - оставить без изменений protected override void OnActionExecuting(ActionExecutingContext filterContext) { _data = (SerializationHelper.Deserialize(Request.Form[_dataKey]) ?? TempData[_dataKey] ?? new ProfileData()) as ProfileData; // and update data from request (data that use input to fields) TryUpdateModel(_data); } protected override void OnResultExecuting(ResultExecutingContext filterContext) { if (filterContext.Result is RedirectToRouteResult) // user click Back, Next, Confirm - method did redirect to other action TempData[_dataKey] = _data; else if (filterContext.Result is ViewResult) // method return View ViewData[_dataKey] = SerializationHelper.Serialize(_data); } }
Теперь у вас есть рабочий мастер (Wizard).
Чтобы лучше понять как работают два последних метода, давайте рассмотрим пример:
Шаг 1.
Пользователь вызывает странцу /WizardProfile/PersonalInfo нашего ВебСайта. Вызывается метод PersonalInfo() класса WizardProfileController. Но перед этим, как ожидали, вызывается метод OnActionExecuting() который начинает смотреть в Request.Form[_dataKey] по заданому ключу, но так как это мы в первый раз зашли на эту страницу.. никакого сохраненного объекта не может быть… поэтому возращается null, смотрим или сохранен в коллекции TempData нужный нам объект, если нет, просто создаем новый.Дальше пробуем обновить данные через TryUpdateModel() но это пока невозможно.. так как это наш первый визит. Происходит выполнение PersonalInfo () в даный момент, он определяет что должен возвратить View(_data), но перед возвращаем, вызывается метод OnResultExecuting(), который записывает в коллекцию ViewData[] сериализованый объект _data. Дальше проиходит return View(_data) метода PersonalInfo ();
Шаг 2.
Начинается отрисовка страницы. Html.Hidden() записывает сериализованый объект.
Шаг 3.
Заполняем данными формы, нажимаем кнопку Next >>. Вызывается метод OnActionExecuting() который смотрит, что в данный момент в Request.Form[_dataKey] уже есть данные, поэтому они десериализуются, и инициализируется переменная _data десерилизованым объектом. Дальше метод PersonalInfo() определяет что нужно сделать редирект на метод ProgrammingInfo(); Вызывается метод OnResultExecuting() который производит теперь уже это действие TempData[_dataKey] = _data; Проиходит return RedirectToAction("ProgrammingInfo");
Шаг 4.
Вызывается метод OnActionExecuting(). Теперь в Request.Form[_dataKey] нету нашего обьекта, так не View вызвала метод котроллера, а был произведен редирект. Но мы находим объект ProfileData в TempData[_dataKey];
Дальше вызывается метод ProgrammingInfo(), который определяет что нужно возвратить View. Вызывается OnResultExecuting(), который сериализует объект ProfileData и заносит в коллекцию ViewData. Происходит return View(_data) из ProgrammingInfo().
Шаг 5.
Идет отрисовка ProgrammingInfo View.
Все!