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


117

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

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

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

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

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

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

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

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


@Jani, ты когда-нибудь догадывался об этом в загрузке? Я бы хотел забрать твой мозг. Я работаю именно над этой проблемой.
Дуг Чемберлен

1
Решение в этом блоге довольно простое и понятное. Он использует div как «шаги», переключая их видимость и ненавязчивую проверку jquery.
Дмитрий Ефименко

Ответы:


229

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


1
@Doug Chamberlain, я использую AutoMapper для преобразования между моими моделями представления и моделями предметной области.
Darin Dimitrov

1
@ Дуг Чемберлен, посмотрите мой обновленный ответ. Надеюсь, это проясняет ситуацию, чем мой первоначальный пост.
Darin Dimitrov

20
+1 @Jani: тебе действительно нужно дать Дарину 50 баллов за этот ответ. Это очень обширно. И ему удалось повторить необходимость использования модели ViewModel, а не моделей домена ;-)
Tom Chantler

3
Я нигде не могу найти атрибут Deserialize ... Также на странице codeplex mvccontrib я нахожу этот 94fa6078a115 Джереми Скиннера 1 августа 2010 г., 17:55 0 Удалите устаревшее связыватель Deserialize. Что вы предлагаете мне сделать?
Чак Норрис

2
Я обнаружил проблему, поскольку не назвал свои представления Step1, Step2 и т. Д. Мои имена названы более значимыми, но не алфавитными. Итак, я получил свои модели в неправильном порядке. Я добавил свойство StepNumber в интерфейс IStepViewModel. Теперь я могу отсортировать это в методе Initialize WizardViewModel.
Джефф Редди,

13

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

Контроллер:

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

11

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

Например, у нас есть процесс трехэтапного мастера.

  1. Пользователь представлен на Шаге 1, на котором есть кнопка с надписью «Далее».
  2. Нажав «Далее», мы делаем запрос 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;
 }
}

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


Да, это интересное решение, но, к сожалению, у нас плохое интернет-соединение на стороне клиента, и он / она должны отправить нам кучу файлов. поэтому мы отклонили это решение ранее.
Jahan

Не могли бы вы сообщить мне, какой объем данных собирается загрузить клиент?
Амит Багга,

Несколько файлов, почти десять, каждый почти по 1 МБ.
Jahan

5

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

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

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

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

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


у вас есть это комплексное решение, которое я могу попробовать? Спасибо
mpora

5

Я хотел поделиться своим собственным способом выполнения этих требований. Я вообще не хотел использовать SessionState и не хотел, чтобы он обрабатывался на стороне клиента, а для метода сериализации требуется 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));
        }
    }
}

Теперь для всех шагов вашего «мастера» вы можете использовать одну и ту же базовую модель и передать свойства модели «Step 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>
}

1
Не могли бы вы пояснить свое решение, предоставив модель представления и контроллер?
Тайлер Дёрден

2

Добавление дополнительной информации из ответа @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" />

    }
}

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


-9

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

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


1
Было бы неплохо, если бы люди, которые голосовали против, также объяснили свою причину.
Мартин

Не голосовал против, но ваш ответ не имеет никакого отношения к вопросу. OP спрашивает, как создать мастер, а вы отвечаете, как обрабатывать ответ в спину.
Dementic

1
Обычно я не голосую, но когда я это делаю, я удостоверяюсь, что он проголосует за :-)
Сухайль Мумтаз Аван
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.