Проблемы многоэтапного процесса регистрации в asp.net mvc (разделенные модели, одна модель)

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

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

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

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

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

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

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

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

7 ответов

Во-первых, вы не должны использовать какие-либо доменные объекты в своих представлениях. Вы должны использовать просмотр моделей. Каждая модель представления будет содержать только те свойства, которые требуются для данного представления, а также атрибуты проверки, специфичные для данного представления. Таким образом, если у вас есть 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 из фьючерсов 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, которые предоставляют функциональные возможности мастера (отличный вариант Stepy). Это в основном вопрос отображения и скрытия div на клиенте, и в этом случае вам больше не нужно беспокоиться о сохранении состояния между шагами.

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


ОБНОВИТЬ:

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

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

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);
    }
}

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

  • Действие Index POST использует [Deserialize] атрибуты из библиотеки Microsoft Futures, поэтому убедитесь, что вы установили MvcContrib NuGet. Вот почему модели моделей должны быть украшены [Serializable] атрибут
  • Действие Index 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. Итак, мы определяем ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml частичный:

@model Step2ViewModel

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

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

Конечно, есть место для улучшения. Действие Index 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. Я оставляю это улучшение в следующий раз:-)

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

Контроллер:

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; }
    }

Я бы посоветовал вам поддерживать состояние Complete Process на клиенте с помощью Jquery.

Например, у нас есть три шага мастера.

  1. Пользователю представлен шаг1, на котором есть кнопка с надписью "Далее"
  2. При нажатии Next мы делаем запрос Ajax и создаем DIV с именем Step2 и загружаем HTML в этот DIV.
  3. На шаге 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;
 }
}

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

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

Вместо этого я создал помощник 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>
}

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

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, в противном случае отправляем обратно на первый шаг с ошибками проверки. На каждом шаге мы отправляем его на следующий шаг, проверяем этот шаг и идем дальше. Теперь некоторые опытные разработчики могут сказать, что мы не можем переходить между шагами, такими как этот, если мы используем атрибуты [Required] или другие аннотации данных между шагами. И вы были бы правы, поэтому удалите ошибки на предметах, которые еще предстоит проверить. как ниже.

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

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

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

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

Добавление дополнительной информации из ответа @Darin.

Что если у вас есть отдельный стиль дизайна для каждого шага и вы хотите сохранить каждый из них в отдельном частичном представлении или что если у вас есть несколько свойств для каждого шага?

При использовании Html.EditorFor у нас есть ограничение, чтобы использовать частичное представление.

Создайте 3 частичных вида под Shared папка с именем: Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

Для краткости я просто выкладываю 1-й внутренний вид, другие шаги такие же, как и у ответа Дарина.

Step1ViewModel.cs

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

  public string LastName { get; set; }

  public string PhoneNo { get; set; }

  public string EmailId { get; set; }

  public int Age { get; set; }

 }

Step1ViewModel.cshtml

 @model WizardPages.ViewModels.Step1ViewModel

<div class="container">
    <h2>Personal Details</h2>

    <div class="form-group">
        <label class="control-label col-sm-2" for="email">First Name:</label>
        <div class="col-sm-10">
            @Html.TextBoxFor(x => x.FirstName)
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-2" for="pwd">Last Name:</label>
        <div class="col-sm-10">
            @Html.TextBoxFor(x => x.LastName)
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-2" for="pwd">Phone No:</label>
        <div class="col-sm-10"> 
            @Html.TextBoxFor(x => x.PhoneNo)
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-2" for="pwd">Email Id:</label>
        <div class="col-sm-10">
            @Html.TextBoxFor(x => x.EmailId)
        </div>
    </div>


</div>

Index.cshtml

@using Microsoft.Web.Mvc
@model WizardPages.ViewModels.WizardViewModel

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

    string viewName = currentStep.ToString().Substring(currentStep.ToString().LastIndexOf('.') + 1);
}

<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.Partial(""+ viewName + "", currentStep);

    if (Model.CurrentStepIndex > 0)
    {

     <input type="submit" value="Previous" name="prev" class="btn btn-warning" />

    }

    if (Model.CurrentStepIndex < Model.Steps.Count - 1)
    {

      <input type="submit" value="Next" name="next" class="btn btn-info" />

    }
    else
    {

      <input type="submit" value="Finish" name="finish" class="btn btn-success" />

    }
}

Если есть какое-то лучшее решение, пожалуйста, прокомментируйте, чтобы другие знали.

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

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

Другие вопросы по тегам