випуск багатоетапного процесу реєстрації в asp.net mvc (розділені моделі перегляду, одна модель)


117

У мене є багатоетапний процес реєстрації , підкріплений одним об’єктом у доменному шарі , у якому є правила перевірки, визначені для властивостей.

Як я повинен перевірити об'єкт домену, коли домен розділений на багато переглядів, і я повинен зберегти об'єкт частково в першому перегляді, коли він розміщений?

Я думав про використання сеансів, але це неможливо, тому що процес тривалий і обсяг даних великий, тому я не хочу використовувати сеанс.

Я думав про збереження всіх даних у реляційному db-пам'яті (з тією ж схемою, що і головний db), а потім перенесення цих даних до головного db, але виникли проблеми, тому що я повинен прокласти маршрут між службами (запитується в представленнях), які працюють з основний db і db пам'яті.

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

ОНОВЛЕННЯ ТА Уточнення:

@Darin Дякую за вашу продуману відповідь, це було саме те, що я робив дотепер. Але, до речі, у мене є запит, в якому є багато вкладених файлів, я створюю, Step2Viewнаприклад, який користувач може асинхронно завантажувати в нього документи, але ці вкладення слід зберігати в таблиці з посиланням на іншу таблицю, яка повинна була бути збережена раніше в Step1View.

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


@Jani, Ви коли-небудь розбиралися у завантаженому творі? Я хотів би забрати ваш мозок. Я працюю над цим точним питанням.
Дуг Чемберлен

1
Рішення в цьому блозі досить просте і прямо вперед. Він використовує divs як "кроки", демонструючи їх видимість та ненав'язливу перевірку 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 - приємний). В основному справа в тому, щоб показати і приховати діви на клієнті, і в цьому випадку вам більше не потрібно турбуватися про збереження стану між кроками.

Але незалежно від того, яке рішення ви обрали, завжди використовуйте моделі перегляду та виконуйте перевірку на цих моделях перегляду. Поки ви наклеюєте атрибути перевірки анотації даних на ваших моделях домену, ви будете дуже важко боротися, оскільки доменні моделі не адаптовані до представлень.


ОНОВЛЕННЯ:

Гаразд, завдяки численним коментарям я роблю висновок, що моя відповідь була не однозначною. І я повинен погодитися. Тож дозвольте спробувати детальніше розробити свій приклад.

Ми могли б визначити інтерфейс, який повинні застосовувати всі крокові моделі перегляду (це лише інтерфейс маркера):

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]атрибутом
  • Дія індексу 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)

Ось як виглядає структура:

введіть тут опис зображення

Звичайно, є можливість для вдосконалення. Дія індексу 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 для перетворення між моїми моделями перегляду та моделями домену.
Дарин Димитров

1
@Doug Chamberlain, будь ласка, дивіться мою оновлену відповідь. Я сподіваюсь, що це робить дещо чіткіше, ніж моя початкова посада.
Дарин Димитров

20
+1 @Jani: вам справді потрібно дати Дарину 50 балів за цю відповідь. Це дуже всебічно. І йому вдалося ще раз підтвердити необхідність використання ViewModel, а не моделей доменів ;-)
Том Чантлер

3
Я не можу ніде знайти атрибут Deserialize ... Також на кодовій сторінці коду mvccontrib я знаходжу цей 94fa6078a115 від Джеремі Скіннера 1 серпня 2010 о 17:55 0 Видалити застаріле десеріалізаційне в'яжуче, що ви пропонуєте мені зробити?
Чак Норріс

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

Я б запропонував вам підтримувати стан повного процесу на клієнті за допомогою Jquery.

Для прикладу, у нас є триетапний майстер.

  1. Користувач, представлений Step1, на якому є кнопка з написом "Далі"
  2. Після наступного клацання ми робимо запит Ajax і створюємо DIV під назвою Step2 і завантажуємо HTML у цей DIV.
  3. На Step3 у нас є кнопка з написом "Готово", натиснувши на кнопку розмістити дані, використовуючи $ .post call.

Таким чином, ви можете легко створити свій доменний об’єкт безпосередньо з форми публікації даних, і якщо дані мають помилки, поверніть дійсне JSON, утримуючи все повідомлення про помилку, і відобразіть їх у діві.

Будь ласка, розділіть кроки

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

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


Так, це цікаве рішення, але, на жаль, у нас поганий зв’язок з Інтернетом на стороні клієнта, і він / вона має надіслати нам купу файлів. тому ми відхилили це рішення раніше.
Джахан

Скажіть, будь ласка, обсяг даних, які клієнт збирається завантажувати.
Аміт Багга

Кілька файлів, майже десять, кожен майже 1 Мб.
Джахан

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

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

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

Сподіваюсь, ви знайдете цей спосіб реалізації майстра набагато простішим у використанні та обслуговуванні, ніж будь-який із згаданих раніше методів.

Дякуємо за прочитане


у вас це є повне рішення, яке я можу спробувати? Дякую
mpora

5

Я хотів поділитися власним способом поводження з цими вимогами. Я взагалі не хотів використовувати SessionState, і не хотів, щоб він обробляв клієнтську сторону, а метод серіалізації вимагає MVC Futures, які я не хотів включати в свій проект.

Натомість я створив HTML Helper, який буде переглядати всі властивості моделі та генерувати нестандартний прихований елемент для кожного. Якщо це складне властивість, то воно буде працювати на ньому рекурсивно.

У вашій формі вони будуть розміщені на контролері разом із новими даними про модель на кожному кроці "майстра".

Я написав це для 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 на контролері, використовуючи атрибут формації. Не включений у приклад нижче, а лише ідея для вас.

У будь-якому випадку є основний приклад:

Ось ваша МОДЕЛЬ

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
Було б добре, якби люди, які голосують проти, також дали свою причину.
Мартін

Ви не проголосували, але ваша відповідь на питання абсолютно не має значення. ОП задає питання про те, як створити майстра, в той час як ви відповідаєте, як обробити відповідь ззаду.
Дементік

1
Я зазвичай не голосую, але коли я це роблю, я переконуюсь, що його підняли :-)
Suhail Mumtaz Awan,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.