ASP.NET MVC Wizard

воскресенье, 27 сентября 2009, Bobasoft

Для начала определим, какие шаги будут у Мастера:

  1. Имя и фамилия
  2. Предпочитаемый язык программирования
  3. Подтверждение введенных данных
  4. Пока информации о завершении

Создаем новый 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.

Все!


Ищите нас в интернетах!

Комментарии

Свежие вакансии