Многоступенчатые процессы регистрации в asp.net mvc (разделенные режимы просмотра, одна модель)

У меня есть процесс многоступенчатой ​​регистрации, поддерживаемый единственным объектом в доменном слое, который имеет правила проверки, определенные для свойств.

Как мне проверить объект домена, когда домен разделен на многие представления, и я должен сохранить объект частично в первом представлении при публикации?

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

Я думал о сохранении всех данных в реляционной внутренней памяти db (с той же схемой, что и основной db), а затем сбрасывая эти данные на главный db, но возникли проблемы, поэтому я должен маршрутизировать между сервисами (запрошенными в представлениях), кто работайте с базовым db и в памяти db.

Я ищу элегантное и чистое решение (точнее, лучшую практику).

ОБНОВЛЕНИЕ И Уточнение:

@Darin Спасибо за ваш продуманный ответ, Это было именно то, что я сделал до сих пор. Но, кстати, у меня есть запрос, в котором есть много вложений, я проектирую Step2View, например. который пользователь может загружать в него документы асинхронно, но эти вложения должны быть сохранены в таблице со ссылочным отношением к другой таблице, которая должна была быть сохранена ранее в Step1View.

Таким образом, я должен сохранить объект домена в Step1 (частично), но я не могу, вызывают поддержку объекта Core Domain, который частично преобразуется в Step1 ViewModel, не может быть сохранен без реквизита, который поступает из преобразованного Step2ViewModel.

Ответ 1

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

public class Step1ViewModel
{
    [Required]
    public string SomeProperty { get; set; }

    ...
}

public class Step2ViewModel
{
    [Required]
    public string SomeOtherProperty { get; set; }

    ...
}

и т.д. Все эти модели просмотра могут быть подкреплены основной моделью просмотра мастера:

public class WizardViewModel
{
    public Step1ViewModel Step1 { get; set; }
    public Step2ViewModel Step2 { get; set; }
    ...
}

тогда вы могли бы выполнять действия контроллера, отображая каждый шаг процесса мастера и передавая основной WizardViewModel в представление. Когда вы находитесь на первом шаге внутри действия контроллера, вы можете инициализировать свойство Step1. Затем в представлении вы должны сгенерировать форму, позволяющую пользователю заполнить свойства о шаге 1. Когда форма отправлена, действие контроллера будет применять правила проверки только для шага 1:

[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
    var model = new WizardViewModel 
    {
        Step1 = step1
    };

    if (!ModelState.IsValid)
    {
        return View(model);
    }
    return View("Step2", model);
}

Теперь в представлении шага 2 вы можете использовать Html.Serialize helper из фьючерсов MVC, чтобы сериализовать шаг 1 в скрытое поле внутри форма (вид ViewState, если хотите):

@using (Html.BeginForm("Step2", "Wizard"))
{
    @Html.Serialize("Step1", Model.Step1)
    @Html.EditorFor(x => x.Step2)
    ...
}

и внутри действия POST для шага 2:

[HttpPost]
public ActionResult Step2(Step2ViewModel step2, [Deserialize] Step1ViewModel step1)
{
    var model = new WizardViewModel 
    {
        Step1 = step1,
        Step2 = step2
    }

    if (!ModelState.IsValid)
    {
        return View(model);
    }
    return View("Step3", model);
}

И так далее, пока вы не дойдете до последнего шага, где у вас будет WizardViewModel, заполненный всеми данными. Затем вы сопоставляете модель представления с моделью домена и передаете ее на сервисный уровень для обработки. Уровень обслуживания может выполнять любые правила проверки и т.д....

Существует и другая альтернатива: использование javascript и размещение всех на одной странице. Существует много jquery plugins, которые предоставляют функции мастера (Stepy является хорошим). Это в основном вопрос отображения и скрытия div на клиенте, и в этом случае вам больше не нужно беспокоиться о сохраняющемся состоянии между шагами.

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


UPDATE:

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

Мы могли бы определить интерфейс, который должны реализовывать все модели просмотра шагов (это просто интерфейс маркера):

public interface IStepViewModel
{
}

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

[Serializable]
public class Step1ViewModel: IStepViewModel
{
    [Required]
    public string Foo { get; set; }
}

[Serializable]
public class Step2ViewModel : IStepViewModel
{
    public string Bar { get; set; }
}

[Serializable]
public class Step3ViewModel : IStepViewModel
{
    [Required]
    public string Baz { get; set; }
}

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

[Serializable]
public class WizardViewModel
{
    public int CurrentStepIndex { get; set; }
    public IList<IStepViewModel> Steps { get; set; }

    public void Initialize()
    {
        Steps = typeof(IStepViewModel)
            .Assembly
            .GetTypes()
            .Where(t => !t.IsAbstract && typeof(IStepViewModel).IsAssignableFrom(t))
            .Select(t => (IStepViewModel)Activator.CreateInstance(t))
            .ToList();
    }
}

Затем переходим к контроллеру:

public class WizardController : Controller
{
    public ActionResult Index()
    {
        var wizard = new WizardViewModel();
        wizard.Initialize();
        return View(wizard);
    }

    [HttpPost]
    public ActionResult Index(
        [Deserialize] WizardViewModel wizard, 
        IStepViewModel step
    )
    {
        wizard.Steps[wizard.CurrentStepIndex] = step;
        if (ModelState.IsValid)
        {
            if (!string.IsNullOrEmpty(Request["next"]))
            {
                wizard.CurrentStepIndex++;
            }
            else if (!string.IsNullOrEmpty(Request["prev"]))
            {
                wizard.CurrentStepIndex--;
            }
            else
            {
                // TODO: we have finished: all the step partial
                // view models have passed validation => map them
                // back to the domain model and do some processing with
                // the results

                return Content("thanks for filling this form", "text/plain");
            }
        }
        else if (!string.IsNullOrEmpty(Request["prev"]))
        {
            // Even if validation failed we allow the user to
            // navigate to previous steps
            wizard.CurrentStepIndex--;
        }
        return View(wizard);
    }
}

Несколько замечаний об этом контроллере:

  • В действии POST индекса используются атрибуты [Deserialize] из библиотеки Microsoft Futures, поэтому убедитесь, что вы установили MvcContrib NuGet. Причина, по которой модели просмотра должны быть украшены атрибутом [Serializable]
  • Действие индекса POST принимает в качестве аргумента интерфейс IStepViewModel, поэтому для этого нужно иметь в виду настраиваемое связующее устройство.

Здесь связанное с ним модельное связующее:

public class StepViewModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var stepTypeValue = bindingContext.ValueProvider.GetValue("StepType");
        var stepType = Type.GetType((string)stepTypeValue.ConvertTo(typeof(string)), true);
        var step = Activator.CreateInstance(stepType);
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => step, stepType);
        return step;
    }
}

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

Это модельное связующее будет зарегистрировано в Application_Start:

ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());

Последний недостающий бит головоломки - это виды. Здесь основной ~/Views/Wizard/Index.cshtml вид:

@using Microsoft.Web.Mvc
@model WizardViewModel

@{
    var currentStep = Model.Steps[Model.CurrentStepIndex];
}

<h3>Step @(Model.CurrentStepIndex + 1) out of @Model.Steps.Count</h3>

@using (Html.BeginForm())
{
    @Html.Serialize("wizard", Model)

    @Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType())
    @Html.EditorFor(x => currentStep, null, "")

    if (Model.CurrentStepIndex > 0)
    {
        <input type="submit" value="Previous" name="prev" />
    }

    if (Model.CurrentStepIndex < Model.Steps.Count - 1)
    {
        <input type="submit" value="Next" name="next" />
    }
    else
    {
        <input type="submit" value="Finish" name="finish" />
    }
}

И все, что вам нужно, чтобы это работало. Конечно, если бы вы хотели, вы могли бы персонализировать внешний вид некоторых или всех шагов мастера, указав шаблон настраиваемого редактора. Например, давайте сделаем это для шага 2. Итак, мы определяем a ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml partial:

@model Step2ViewModel

Special Step 2
@Html.TextBoxFor(x => x.Bar)

Вот как выглядит структура:

enter image description here

Конечно, есть возможности для улучшения. Действие POST индекса выглядит как s..t. Там слишком много кода. Дальнейшее упрощение будет связано с перемещением всех объектов инфраструктуры, таких как индекс, текущее управление индексами, копирование текущего шага в мастер,... в другое связующее устройство. Итак, в итоге мы получим:

[HttpPost]
public ActionResult Index(WizardViewModel wizard)
{
    if (ModelState.IsValid)
    {
        // TODO: we have finished: all the step partial
        // view models have passed validation => map them
        // back to the domain model and do some processing with
        // the results
        return Content("thanks for filling this form", "text/plain");
    }
    return View(wizard);
}

что больше похоже на действия POST. Я оставляю это улучшение в следующий раз: -)

Ответ 2

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

Контроллер:

public ActionResult Step1()
{
    if (Session["wizard"] != null)
    {
        WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
        return View(wiz.Step1);
    }
    return View();
}

[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
    if (ModelState.IsValid)
    {
        WizardProductViewModel wiz = new WizardProductViewModel();
        wiz.Step1 = step1;
        //Store the wizard in session
        Session["wizard"] = wiz;
        return RedirectToAction("Step2");
    }
    return View(step1);
}

public ActionResult Step2()
{
    if (Session["wizard"] != null)
    {
        WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
        return View(wiz.Step2);
    }
    return View();
}

[HttpPost]
public ActionResult Step2(Step2ViewModel step2)
{
    if (ModelState.IsValid)
    {
        //Pull the wizard from session
        WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
        wiz.Step2 = step2;
        //Store the wizard in session
        Session["wizard"] = wiz;
        //return View("Step3");
        return RedirectToAction("Step3");
    }
    return View(step2);
}

public ActionResult Step3()
{
    WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
    return View(wiz.Step3);
}

[HttpPost]
public ActionResult Step3(Step3ViewModel step3)
{
    if (ModelState.IsValid)
    {
        //Pull the wizard from session
        WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
        wiz.Step3 = step3;
        //Save the data
        Product product = new Product
        {
            //Binding with view models
            Name = wiz.Step1.Name,
            ListPrice = wiz.Step2.ListPrice,
            DiscontinuedDate = wiz.Step3.DiscontinuedDate
        };

        db.Products.Add(product);
        db.SaveChanges();
        return RedirectToAction("Index", "Product");
    }
    return View(step3);
}

Модели:

 [Serializable]
    public class Step1ViewModel 
    {
        [Required]
        [MaxLength(20, ErrorMessage="Longueur max de 20 caractères")]
        public string Name { get; set; }

    }

    [Serializable]
    public class Step2ViewModel
    {
        public Decimal ListPrice { get; set; }

    }

    [Serializable]
    public class Step3ViewModel
    {
        public DateTime? DiscontinuedDate { get; set; }
    }

    [Serializable]
    public class WizardProductViewModel
    {
        public Step1ViewModel Step1  { get; set; }
        public Step2ViewModel Step2  { get; set; }
        public Step3ViewModel Step3  { get; set; }
    }

Ответ 3

Я предлагаю вам поддерживать состояние полного процесса на клиенте с помощью JQuery.

Для примера у нас есть процесс с тремя шагами.

  • Пользователь в представленном шаге 1, на котором есть кнопка с надписью "Далее"
  • On Clicking Next Мы делаем запрос Ajax и создаем DIV под названием Step2 и загружаем HTML в этот DIV.
  • На шаге 3 у нас есть кнопка с надписью "Готово" при нажатии кнопки "Поместить данные" с помощью вызова $.post.

Таким образом, вы можете легко создать свой доменный объект непосредственно из данных сообщения формы и в случае, если данные имеют ошибки, возвращают действительный JSON, сохраняя все сообщение об ошибке и отображая их в div.

Разделите шаги

public class Wizard 
{
  public Step1 Step1 {get;set;}
  public Step2 Step2 {get;set;}
  public Step3 Step3 {get;set;}
}

public ActionResult Step1(Step1 step)
{
  if(Model.IsValid)
 {
   Wizard wiz = new Wizard();
   wiz.Step1 = step;
  //Store the Wizard in Session;
  //Return the action
 }
}

public ActionResult Step2(Step2 step)
{
 if(Model.IsValid)
 {
   //Pull the Wizard From Session
   wiz.Step2=step;
 }
}

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

Ответ 4

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

public class MyModel
{
     [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
     public Guid Id { get; set };
     public string StepOneData { get; set; }
     public string StepTwoData { get; set; }
}

Вышеупомянутое коллирование просто глупо, поэтому замените свои поля там. Затем мы начинаем с простого действия, которое инициирует наш мастер.

    public ActionResult WizardStep1()
    {
        return View(new MyModel());
    }

Это вызывает представление "WizardStep1.cshtml(если используется бритва), вы можете использовать мастер создания шаблонов, если хотите. Мы просто перенаправим сообщение на другое действие.

<WizardStep1.cshtml>
@using (Html.BeginForm("WizardStep2", "MyWizard")) {

Следует отметить, что мы опубликуем это для другого действия; действие WizardStep2

    [HttpPost]
    public ActionResult WizardStep2(MyModel myModel)
    {
        return ModelState.IsValid ? View(myModel) : View("WizardStep1", myModel);
    }

В этом действии мы проверяем, действительно ли наша модель действительна, и если мы отправим ее в наш WizardStep2.cshtml, мы вернем его на первый шаг с ошибками проверки. На каждом шаге мы отправляем его на следующий шаг, проверяем этот шаг и переходим. Теперь некоторые опытные разработчики могут сказать, что мы не можем двигаться между такими шагами, как это, если мы используем атрибуты [Обязательные] или другие аннотации данных между шагами. И вы были бы правы, поэтому удалите ошибки на элементах, которые еще предстоит проверить. как показано ниже.

    [HttpPost]
    public ActionResult WizardStep3(MyModel myModel)
    {
        foreach (var error in ModelState["StepTwoData"].Errors)
        {
            ModelState["StepTwoData"].Errors.Remove(error);
        }

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

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

Спасибо за чтение.

Ответ 5

Я хотел поделиться своим собственным способом решения этих требований. Я вообще не хотел использовать SessionState, и не хотел, чтобы он обрабатывался на стороне клиента, а для метода serialize требуется MVC Futures, который я не хотел включать в свой проект.

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

В вашей форме они будут отправляться на контроллер вместе с новыми данными модели на каждом шаге "Мастер".

Я написал это для MVC 5.

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Web;
using System.Web.Routing;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Reflection;

namespace YourNamespace
{
    public static class CHTML
    {
        public static MvcHtmlString HiddenClassFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
        {
            return HiddenClassFor(html, expression, null);
        }

        public static MvcHtmlString HiddenClassFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
        {
            ModelMetadata _metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);

            if (_metaData.Model == null)
                return MvcHtmlString.Empty;

            RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;

            return MvcHtmlString.Create(HiddenClassFor(html, expression, _metaData, _dict).ToString());
        }

        private static StringBuilder HiddenClassFor<TModel>(HtmlHelper<TModel> html, LambdaExpression expression, ModelMetadata metaData, IDictionary<string, object> htmlAttributes)
        {
            StringBuilder _sb = new StringBuilder();

            foreach (ModelMetadata _prop in metaData.Properties)
            {
                Type _type = typeof(Func<,>).MakeGenericType(typeof(TModel), _prop.ModelType);
                var _body = Expression.Property(expression.Body, _prop.PropertyName);
                LambdaExpression _propExp = Expression.Lambda(_type, _body, expression.Parameters);

                if (!_prop.IsComplexType)
                {
                    string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(_propExp));
                    string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(_propExp));
                    object _value = _prop.Model;

                    _sb.Append(MinHiddenFor(_id, _name, _value, htmlAttributes));
                }
                else
                {
                    if (_prop.ModelType.IsArray)
                        _sb.Append(HiddenArrayFor(html, _propExp, _prop, htmlAttributes));
                    else if (_prop.ModelType.IsClass)
                        _sb.Append(HiddenClassFor(html, _propExp, _prop, htmlAttributes));
                    else
                        throw new Exception(string.Format("Cannot handle complex property, {0}, of type, {1}.", _prop.PropertyName, _prop.ModelType));
                }
            }

            return _sb;
        }

        public static MvcHtmlString HiddenArrayFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
        {
            return HiddenArrayFor(html, expression, null);
        }

        public static MvcHtmlString HiddenArrayFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
        {
            ModelMetadata _metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);

            if (_metaData.Model == null)
                return MvcHtmlString.Empty;

            RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;

            return MvcHtmlString.Create(HiddenArrayFor(html, expression, _metaData, _dict).ToString());
        }

        private static StringBuilder HiddenArrayFor<TModel>(HtmlHelper<TModel> html, LambdaExpression expression, ModelMetadata metaData, IDictionary<string, object> htmlAttributes)
        {
            Type _eleType = metaData.ModelType.GetElementType();
            Type _type = typeof(Func<,>).MakeGenericType(typeof(TModel), _eleType);

            object[] _array = (object[])metaData.Model;

            StringBuilder _sb = new StringBuilder();

            for (int i = 0; i < _array.Length; i++)
            {
                var _body = Expression.ArrayIndex(expression.Body, Expression.Constant(i));
                LambdaExpression _arrayExp = Expression.Lambda(_type, _body, expression.Parameters);
                ModelMetadata _valueMeta = ModelMetadata.FromLambdaExpression((dynamic)_arrayExp, html.ViewData);

                if (_eleType.IsClass)
                {
                    _sb.Append(HiddenClassFor(html, _arrayExp, _valueMeta, htmlAttributes));
                }
                else
                {
                    string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(_arrayExp));
                    string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(_arrayExp));
                    object _value = _valueMeta.Model;

                    _sb.Append(MinHiddenFor(_id, _name, _value, htmlAttributes));
                }
            }

            return _sb;
        }

        public static MvcHtmlString MinHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
        {
            return MinHiddenFor(html, expression, null);
        }

        public static MvcHtmlString MinHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
        {
            string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(expression));
            string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression));
            object _value = ModelMetadata.FromLambdaExpression(expression, html.ViewData).Model;
            RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;

            return MinHiddenFor(_id, _name, _value, _dict);
        }

        public static MvcHtmlString MinHiddenFor(string id, string name, object value, IDictionary<string, object> htmlAttributes)
        {
            TagBuilder _input = new TagBuilder("input");
            _input.Attributes.Add("id", id);
            _input.Attributes.Add("name", name);
            _input.Attributes.Add("type", "hidden");

            if (value != null)
            {
                _input.Attributes.Add("value", value.ToString());
            }

            if (htmlAttributes != null)
            {
                foreach (KeyValuePair<string, object> _pair in htmlAttributes)
                {
                    _input.MergeAttribute(_pair.Key, _pair.Value.ToString(), true);
                }
            }

            return new MvcHtmlString(_input.ToString(TagRenderMode.SelfClosing));
        }
    }
}

Теперь для всех шагов вашего "мастера" вы можете использовать одну и ту же базовую модель и передать свойства модели "Шаг 1,2,3" в помощник @Html.HiddenClassFor, используя выражение лямбда.

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

В любом случае вот пример:

Вот ваш МОДЕЛЬ

public class WizardModel
{
    // you can store additional properties for your "wizard" / parent model here
    // these properties can be saved between pages by storing them in the form using @Html.MinHiddenFor(m => m.WizardID)
    public int? WizardID { get; set; }

    public string WizardType { get; set; }

    [Required]
    public Step1 Step1 { get; set; }

    [Required]
    public Step2 Step2 { get; set; }

    [Required]
    public Step3 Step3 { get; set; }

    // if you want to use the same model / view / controller for EDITING existing data as well as submitting NEW data here is an example of how to handle it
    public bool IsNew
    {
        get
        {
            return WizardID.HasValue;
        }
    }
}

public class Step1
{
    [Required]
    [MaxLength(32)]
    [Display(Name = "First Name")]
    public string FirstName { get; set; }

    [Required]
    [MaxLength(32)]
    [Display(Name = "Last Name")]
    public string LastName { get; set; }
}

public class Step2
{
    [Required]
    [MaxLength(512)]
    [Display(Name = "Biography")]
    public string Biography { get; set; }
}

public class Step3
{        
    // lets have an array of strings here to shake things up
    [Required]
    [Display(Name = "Your Favorite Foods")]
    public string[] FavoriteFoods { get; set; }
}

Вот ваш КОНТРОЛЛЕР

public class WizardController : Controller
{
    [HttpGet]
    [Route("wizard/new")]
    public ActionResult New()
    {
        WizardModel _model = new WizardModel()
        {
            WizardID = null,
            WizardType = "UserInfo"
        };

        return View("Step1", _model);
    }

    [HttpGet]
    [Route("wizard/edit/{wizardID:int}")]
    public ActionResult Edit(int wizardID)
    {
        WizardModel _model = database.GetData(wizardID);

        return View("Step1", _model);
    }

    [HttpPost]
    [Route("wizard/step1")]
    public ActionResult Step1(WizardModel model)
    {
        // just check if the values in the step1 model are valid
        // shouldn't use ModelState.IsValid here because that would check step2 & step3.
        // which isn't entered yet
        if (ModelState.IsValidField("Step1"))
        {
            return View("Step2", model);
        }

        return View("Step1", model);
    }

    [HttpPost]
    [Route("wizard/step2")]
    public ActionResult Step2(WizardModel model)
    {
        if (ModelState.IsValidField("Step2"))
        {
            return View("Step3", model);
        }

        return View("Step2", model);
    }

    [HttpPost]
    [Route("wizard/step3")]
    public ActionResult Step3(WizardModel model)
    {
        // all of the data for the wizard model is complete.
        // so now we check the entire model state
        if (ModelState.IsValid)
        {
            // validation succeeded. save the data from the model.
            // the model.IsNew is just if you want users to be able to
            // edit their existing data.
            if (model.IsNew)
                database.NewData(model);
            else
                database.EditData(model);

            return RedirectToAction("Success");
        }

        return View("Step3", model);
    }
}

Вот ваши ПРОСМОТРЫ

Шаг 1

@model WizardModel

@{
    ViewBag.Title = "Step 1";
}

@using (Html.BeginForm("Step1", "Wizard", FormMethod.Post))
{
    @Html.MinHiddenFor(m => m.WizardID)
    @Html.MinHiddenFor(m => m.WizardType)

    @Html.LabelFor(m => m.Step1.FirstName)
    @Html.TextBoxFor(m => m.Step1.FirstName)

    @Html.LabelFor(m => m.Step1.LastName)
    @Html.TextBoxFor(m => m.Step1.LastName)

    <button type="submit">Submit</button>
}

Шаг 2

@model WizardModel

@{
    ViewBag.Title = "Step 2";
}

@using (Html.BeginForm("Step2", "Wizard", FormMethod.Post))
{
    @Html.MinHiddenFor(m => m.WizardID)
    @Html.MinHiddenFor(m => m.WizardType)
    @Html.HiddenClassFor(m => m.Step1)

    @Html.LabelFor(m => m.Step2.Biography)
    @Html.TextAreaFor(m => m.Step2.Biography)

    <button type="submit">Submit</button>
}

Шаг 3

@model WizardModel

@{
    ViewBag.Title = "Step 3";
}

@using (Html.BeginForm("Step3", "Wizard", FormMethod.Post))
{
    @Html.MinHiddenFor(m => m.WizardID)
    @Html.MinHiddenFor(m => m.WizardType)
    @Html.HiddenClassFor(m => m.Step1)
    @Html.HiddenClassFor(m => m.Step2)

    @Html.LabelFor(m => m.Step3.FavoriteFoods)
    @Html.ListBoxFor(m => m.Step3.FavoriteFoods,
        new SelectListItem[]
        {
            new SelectListItem() { Value = "Pizza", Text = "Pizza" },
            new SelectListItem() { Value = "Sandwiches", Text = "Sandwiches" },
            new SelectListItem() { Value = "Burgers", Text = "Burgers" },
        });

    <button type="submit">Submit</button>
}

Ответ 6

Один из вариантов - создать набор идентичных таблиц, которые будут хранить данные, собранные на каждом шаге. Затем на последнем шаге, если все будет хорошо, вы можете создать реальный объект, скопировав временные данные и сохраните его.

Другое - создать Value Objects для каждого шага и сохранить затем в Cache или Session. Тогда, если все пойдет хорошо, вы можете создать свой объект Domain из них и сохранить его