asp.net-mvc asp.net mvc 中的多步注册过程问题(拆分视图模型,单个模型)

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6402628/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 01:16:38  来源:igfitidea点击:

multi-step registration process issues in asp.net mvc (split viewmodels, single model)

asp.net-mvc

提问by Jahan

I have a multi-step registration process, backed by a single object in domain layer, which have validation rules defined on properties.

我有一个多步骤注册过程,由域层中单个对象支持,它在属性上定义了验证规则。

How should I validate the domain object when the domain is split across many views, and I have to save the object partially in the first view when posted?

当域跨多个视图拆分时,我应该如何验证域对象,并且发布时必须在第一个视图中部分保存对象?

I thought about using Sessions but that's not possible cause the process is lengthy and amount of data is high, So I don't want to use session.

我想过使用 Sessions 但这是不可能的,因为这个过程很长而且数据量很大,所以我不想使用 session。

I thought about saving all the data in an relational in-memory db (with the same schema as main db) and then flushing that data to main db but issues arisen cause I should route between services (requested in the views) who work with the main db and in-memory db.

我想过将所有数据保存在关系内存数据库中(与主数据库具有相同的架构),然后将该数据刷新到主数据库,但问题出现了,因为我应该在服务之间路由(在视图中请求)与主数据库和内存数据库。

I'm looking for an elegant and clean solution (more precisely a best practice).

我正在寻找一个优雅和干净的解决方案(更准确地说是最佳实践)。

UPDATE AND Clarification:

更新和澄清:

@Darin Thank you for your thoughtful reply, That was exactly what I've done till now. But incidentally I've got a request which have many attachments in it, I design a Step2Viewe.g. which user can upload documents in it asynchronously , but those attachments should be saved in a table with referential relation to another table that should have been saved before in Step1View.

@Darin 感谢您的深思熟虑的答复,这正是我迄今为止所做的。但顺便说一句,我有一个请求,其中有很多附件,我设计了一个Step2View例如用户可以异步上传其中的文档,但是这些附件应该保存在一个表中,并与另一个应该在之前保存的表有参考关系Step1View.

Thus I should save the domain object in Step1(partially), But I can't, cause the backed Core Domain object which is mapped partially to a Step1's ViewModel can't be saved without props that come from converted Step2ViewModel.

因此,我应该Step1(部分)保存域对象,但我不能,导致部分映射到 Step1 的 ViewModel 的支持核心域对象无法在没有来自 convert 的道具的情况下保存Step2ViewModel

回答by Darin Dimitrov

First you shouldn't be using any domain objects in your views. You should be using view models. Each view model will contain only the properties that are required by the given view as well as the validation attributes specific to this given view. So if you have 3 steps wizard this means that you will have 3 view models, one for each step:

首先,您不应在视图中使用任何域对象。您应该使用视图模型。每个视图模型将仅包含给定视图所需的属性以及特定于该给定视图的验证属性。因此,如果您有 3 个步骤向导,这意味着您将拥有 3 个视图模型,每个步骤一个:

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

    ...
}

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

    ...
}

and so on. All those view models could be backed by a main wizard view model:

等等。所有这些视图模型都可以由主向导视图模型支持:

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

then you could have controller actions rendering each step of the wizard process and passing the main WizardViewModelto the view. When you are on the first step inside the controller action you could initialize the Step1property. Then inside the view you would generate the form allowing the user to fill the properties about step 1. When the form is submitted the controller action will apply the validation rules for step 1 only:

然后你可以让控制器动作呈现向导过程的每个步骤并将主要内容传递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);
}

Now inside the step 2 view you could use the Html.Serialize helperfrom MVC futures in order to serialize step 1 into a hidden field inside the form (sort of a ViewState if you wish):

现在在第 2 步视图中,您可以使用MVC 期货中的Html.Serialize 助手,以便将第 1 步序列化为表单内的隐藏字段(如果您愿意,可以使用 ViewState):

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

and inside the POST action of step2:

在 step2 的 POST 操作中:

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

And so on until you get to the last step where you will have the WizardViewModelfilled with all the data. Then you will map the view model to your domain model and pass it to the service layer for processing. The service layer might perform any validation rules itself and so on ...

依此类推,直到您到达最后一步,您将WizardViewModel填充所有数据。然后,您将视图模型映射到您的域模型,并将其传递给服务层进行处理。服务层可能会自己执行任何验证规则等等......

There is also another alternative: using javascript and putting all on the same page. There are many jquery pluginsout there that provide wizard functionality (Stepyis a nice one). It's basically a matter of showing and hiding divs on the client in which case you no longer need to worry about persisting state between the steps.

还有另一种选择:使用javascript并将所有内容放在同一页面上。有许多提供向导功能的jquery 插件Stepy是一个不错的插件)。这基本上是在客户端上显示和隐藏 div 的问题,在这种情况下,您不再需要担心步骤之间的持久状态。

But no matter what solution you choose always use view models and perform the validation on those view models. As long you are sticking data annotation validation attributes on your domain models you will struggle very hard as domain models are not adapted to views.

但是无论您选择哪种解决方案,请始终使用视图模型并在这些视图模型上执行验证。只要您在域模型上粘贴数据注释验证属性,您就会非常努力,因为域模型不适应视图。



UPDATE:

更新:

OK, due to the numerous comments I draw the conclusion that my answer was not clear. And I must agree. So let me try to further elaborate my example.

好的,由于众多评论,我得出的结论是我的答案不清楚。我必须同意。所以让我试着进一步阐述我的例子。

We could define an interface which all step view models should implement (it's just a marker interface):

我们可以定义一个所有步骤视图模型都应该实现的接口(它只是一个标记接口):

public interface IStepViewModel
{
}

then we would define 3 steps for the wizard where each step would of course contain only the properties that it requires as well as the relevant validation attributes:

然后我们将为向导定义 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; }
}

next we define the main wizard view model which consists of a list of steps and a current step index:

接下来我们定义主向导视图模型,它由步骤列表和当前步骤索引组成:

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

Then we move on to the controller:

然后我们转到控制器:

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

Couple of remarks about this controller:

关于这个控制器的一些评论:

  • The Index POST action uses the [Deserialize]attributes from the Microsoft Futures library so make sure you have installed the MvcContribNuGet. That's the reason why view models should be decorated with the [Serializable]attribute
  • The Index POST action takes as argument an IStepViewModelinterface so for this to make sense we need a custom model binder.
  • Index POST 操作使用[Deserialize]Microsoft Futures 库中的属性,因此请确保您已安装MvcContribNuGet。这就是为什么视图模型应该用[Serializable]属性装饰的原因
  • Index POST 操作将一个IStepViewModel接口作为参数,因此为了使这一点有意义,我们需要一个自定义模型绑定器。

Here's the associated model binder:

这是相关的模型绑定器:

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

This binder uses a special hidden field called StepType which will contain the concrete type of each step and which we will send on each request.

这个活页夹使用一个称为 StepType 的特殊隐藏字段,它将包含每个步骤的具体类型,我们将在每个请求中发送它。

This model binder will be registered in Application_Start:

此模型绑定器将注册在Application_Start

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

The last missing bit of the puzzle are the views. Here's the main ~/Views/Wizard/Index.cshtmlview:

拼图的最后一个缺失部分是视图。这是主要~/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" />
    }
}

And that's all you need to make this working. Of course if you wanted you could personalize the look and feel of some or all steps of the wizard by defining a custom editor template. For example let's do it for step 2. So we define a ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtmlpartial:

这就是您要使其正常工作所需的全部内容。当然,如果您愿意,您可以通过定义自定义编辑器模板来个性化向导的某些或所有步骤的外观。例如,让我们为第 2 步做。所以我们定义了一个~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml部分:

@model Step2ViewModel

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

Here's how the structure looks like:

下面是结构的样子:

enter image description here

在此处输入图片说明

Of course there is room for improvement. The Index POST action looks like s..t. There's too much code in it. A further simplification would involve into moving all the infrastructure stuff like index, current index management, copying of the current step into the wizard, ... into another model binder. So that finally we end up with:

当然还有改进的余地。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);
}

which is more how POST actions should look like. I am leaving this improvement for the next time :-)

这更像是 POST 操作的样子。我将在下一次离开这个改进:-)

回答by Arno 2501

To supplement on Amit Bagga's answer you will find below what I did. Even if less elegant I find this way simpler than Darin's answer.

为了补充 Amit Bagga 的回答,您将在下面找到我所做的。即使不那么优雅,我发现这种方式比 Darin 的答案更简单。

Controller :

控制器 :

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

Models :

楷模 :

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

回答by Amit Bagga

I would suggest you to maintain the state of Complete Process on the client using Jquery.

我建议您使用 Jquery 在客户端上维护 Complete Process 的状态。

For Example we have a Three Step Wizard process.

例如,我们有一个三步向导过程。

  1. The user in presented with the Step1 on which has a button Labeled "Next"
  2. On Clicking Next We make an Ajax Request and Create a DIV called Step2 and load the HTML into that DIV.
  3. On the Step3 we have a Button labeled "Finished" on Clicking on the button post the data using $.post call.
  1. 用户在 Step1 上有一个标记为“下一步”的按钮
  2. 单击 Next 我们发出一个 Ajax 请求并创建一个名为 Step2 的 DIV,并将 HTML 加载到该 DIV 中。
  3. 在 Step3 上,我们有一个标记为“完成”的按钮,点击按钮使用 $.post 调用发布数据。

This way you can easily build your domain object directly from the form post data and in case the data has errors return valid JSON holding all the error message and display them in a div.

通过这种方式,您可以直接从表单发布数据轻松构建域对象,如果数据有错误,则返回包含所有错误消息的有效 JSON 并将它们显示在 div 中。

Please split the Steps

请拆分步骤

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

The Above is just a demonstration which will help you achieve the end result. On the Final Step you have to create the Domain Object and populate the correct values from the Wizard Object and Store into the database.

以上只是一个演示,它将帮助您实现最终结果。在最后一步,您必须创建域对象并从向导对象和存储中填充正确的值到数据库中。

回答by Darroll

Wizards are just simple steps in processing a simple model. There is no reason to create multiple models for a wizard. All you would do is create a single model and pass it between actions in a single controller.

向导只是处理简单模型的简单步骤。没有理由为一个向导创建多个模型。您要做的就是创建一个模型并在单个控制器中的操作之间传递它。

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

The above coed is stupid simple so replace your fields in there. Next we start with a simple action that initiates our wizard.

上面的 coed 很简单,所以替换你的字段。接下来,我们从启动向导的简单操作开始。

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

This calls the view "WizardStep1.cshtml (if using razor that is). You can use the create template wizard if you want. We will just be redirecting the post to a different action.

这将调用视图“WizardStep1.cshtml(如果使用 razor)。如果需要,您可以使用创建模板向导。我们只是将帖子重定向到不同的操作。

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

The thing of note is that we will be posting this to a different action; the WizardStep2 action

值得注意的是,我们将将此发布到不同的操作中;WizardStep2 操作

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

In this action we check if our model is valid, and if so we send it to our WizardStep2.cshtml view else we send it back to step one with the validation errors. In each step we send it to the next step, validate that step and move on. Now some savvy developers might say well we can't move between steps such as this if we use [Required] attributes or other data annotations between steps. And you would be right, so remove the errors on items that are yet to be checked. like below.

在这个动作中,我们检查我们的模型是否有效,如果是,我们将它发送到我们的 WizardStep2.cshtml 视图,否则我们将它发送回带有验证错误的第一步。在每个步骤中,我们将其发送到下一步,验证该步骤并继续。现在,一些精明的开发人员可能会说,如果我们在步骤之间使用 [Required] 属性或其他数据注释,我们就不能在这样的步骤之间移动。你是对的,所以删除尚未检查的项目上的错误。像下面。

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

Finally we would save the model once to the data store. This also prevents a user that starts a wizard but doesn't finish it not to save incomplete data to the database.

最后,我们将模型一次保存到数据存储中。这还可以防止启动向导但未完成向导的用户不将不完整的数据保存到数据库中。

I hope you find this method of implementing a wizard much easier to use and maintain than any of the previously mentioned methods.

我希望您发现这种实现向导的方法比前面提到的任何方法都更易于使用和维护。

Thanks for reading.

谢谢阅读。

回答by ArcadeRenegade

I wanted to share my own way of handling these requirements. I did not want to use SessionState at all, nor did I want it handled client side, and the serialize method requires MVC Futures which I did not want to have to include in my project.

我想分享我自己处理这些要求的方式。我根本不想使用 SessionState,也不希望它处理客户端,并且序列化方法需要 MVC Futures,我不想将其包含在我的项目中。

Instead I built an HTML Helper that will iterate through all of the properties of the model and generate a custom hidden element for each one. If it is a complex property then it will run recursively on it.

相反,我构建了一个 HTML Helper,它将遍历模型的所有属性并为每个属性生成一个自定义隐藏元素。如果它是一个复杂的属性,那么它将在其上递归运行。

In your form they will be posted to the controller along with the new model data at each "wizard" step.

在您的表单中,它们将与每个“向导”步骤的新模型数据一起发布到控制器。

I wrote this for MVC 5.

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

Now for all steps of your "wizard" you can use the same base model and pass the "Step 1,2,3" model properties into the @Html.HiddenClassFor helper using a lambda expression.

现在,对于“向导”的所有步骤,您可以使用相同的基本模型,并使用 lambda 表达式将“步骤 1、2、3”模型属性传递到 @Html.HiddenClassFor 助手中。

You can even have a back button at each step if you want to. Just have a back button in your form that will post it to a StepNBack action on the controller using the formaction attribute. Not included in the below example but just an idea for you.

如果您愿意,您甚至可以在每一步都有一个后退按钮。只需在您的表单中有一个后退按钮,该按钮将使用 formaction 属性将其发布到控制器上的 StepNBack 操作。不包括在下面的例子中,只是给你的一个想法。

Anyways here is a basic example:

无论如何,这是一个基本示例:

Here is your MODEL

这是你的模型

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

Here is your CONTROLLER

这是你的控制器

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

Here are your VIEWS

这是你的观点

Step 1

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

Step 2

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

Step 3

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

回答by shaijut

Adding more info from @Darin's answer.

从@Darin 的回答中添加更多信息。

What if you have separate design style for each steps and wanted maintain each in separate partial view or what if you have multiple properties for each step ?

如果您对每个步骤都有单独的设计风格并希望在单独的局部视图中维护每个步骤,或者如果每个步骤都有多个属性怎么办?

While using Html.EditorForwe have limitation to use partial view.

使用时Html.EditorFor我们有使用局部视图的限制。

Create 3 Partial Views under Sharedfolder named : Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

Shared名为 的文件夹下创建 3 个局部视图:Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

For brevity I just posting 1st patial view, other steps are same as Darin's answer.

为简洁起见,我只发布了第一个 patial 视图,其他步骤与 Darin 的回答相同。

Step1ViewModel.cs

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

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

索引.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" />

    }
}

If there some better solution, please comment to let others know.

如果有更好的解决方案,请发表评论让其他人知道。

回答by Amila Silva

One option is to create set of identical tables that will store the data collected in each step. Then in the last step if all goes well you can create the real entity by copying the temporary data and store it.

一种选择是创建一组相同的表,用于存储在每个步骤中收集的数据。然后在最后一步,如果一切顺利,您可以通过复制临时数据并存储它来创建真实实体。

Other is to create Value Objectsfor each step and store then in Cacheor Session. Then if all goes well you can create your Domain object from them and save it

另一种是Value Objects为每个步骤创建然后存储在Cacheor 中Session。然后,如果一切顺利,您可以从它们创建域对象并保存它