Net mvc (分割视图模型,单一模型)中的多步注册过程问题

我有一个 多步注册过程多步注册过程,后面有一个 域层中的单个对象,它有定义在属性上的验证规则。

当域被分割到多个视图时,我应该如何验证域对象, 我必须保存对象部分在第一个视图时张贴?

我考虑过使用会话,但这是不可能的,因为这个过程很长,数据量很大,所以我不想使用会话。

我考虑过将所有数据保存在一个关系内存中的 db 中(使用与 main db 相同的模式) ,然后将该数据刷新到 main db,但是由于我应该在处理 main db 和内存中 db 的服务(视图中请求的)之间进行路由,所以出现了一些问题。

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

更新及澄清:

@ 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.

54006 次浏览

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

其他方法是为每个步骤创建 Value Objects,然后存储在 CacheSession中。然后,如果一切顺利,您可以从他们创建您的域对象,并保存它

首先,您不应该在视图中使用任何域对象。您应该使用视图模型。每个视图模型将只包含给定视图所需的属性以及特定于该给定视图的验证属性。因此,如果你有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);
}

Now inside the step 2 view you could use the 序列化助手 from MVC futures in order to serialize step 1 into a hidden field inside the form (sort of a ViewState if you wish):

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

在第2步的 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);
}

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

还有另一种选择: 使用 javascript 并将所有内容放在同一个页面上。有许多 jquery plugins提供向导功能(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);
}
}

关于这个控制器的几点说明:

  • IndexPOST 操作使用来自 MicrosoftFutures 库的 [Deserialize]属性,因此请确保您已经安装了 MvcContribNuGet。这就是为什么视图模型应该用 [Serializable]属性来装饰的原因
  • The Index POST action takes as argument an IStepViewModel interface so for this to make sense we need a custom 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;
}
}

这个活页夹使用一个称为 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" />
}
}

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.cshtml partial:

@model Step2ViewModel


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

下面是它的结构:

enter image description here

当然还有改进的空间。IndexPOST 操作类似于 s。里面的代码太多了。进一步的简化将涉及到将所有基础结构内容(如索引、当前索引管理、将当前步骤复制到向导中)移动到另一个模型绑定器中。最后我们得出结论:

[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 操作应该看起来的样子。我把这个改进留给下一次: -)

我建议您使用 Jquery 在客户机上维护 CompleteProcess 的状态。

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

  1. 用户在显示的步骤1上有一个按钮标记为“下一步”
  2. 在单击 Next 时,我们创建一个 Ajax 请求并创建一个名为 Step2的 DIV,然后将 HTML 加载到该 DIV 中。
  3. 在第3步,我们有一个按钮标签“完成”上点击按钮发布数据使用 $. post 调用。

通过这种方式,您可以很容易地直接从表单 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;
}
}

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

作为对 Amit Bagga 回答的补充,你会发现下面我所做的。即使不那么优雅,我发现这种方式比达林的回答更简单。

总监:

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

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

上述男女混合是愚蠢的简单,所以更换你的领域在那里。接下来,我们从启动向导的简单操作开始。

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

这将视图调用为“ WizardStep1.cshtml (如果使用剃须刀)”。如果需要,可以使用创建模板向导。我们只是将邮件重定向到一个不同的动作。

<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

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

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

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

谢谢你的阅读。

我想分享我自己处理这些需求的方法。我根本不想使用 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));
}
}
}

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

如果你愿意,你甚至可以在每一步都设置一个后退按钮。只需在表单中设置一个后退按钮,该按钮将使用 formaction 属性将其发布到控制器上的 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);
}
}

Here are your VIEWS

第一步

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

第二步

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

第三步

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

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

如果每个步骤都有单独的设计样式,并希望在单独的部分视图中维护每个步骤,或者如果每个步骤都有多个属性,该怎么办?

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

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

为了简短起见,我只发布了第一个病态视图,其他步骤与达林的答案相同。

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" />


}
}

如果有更好的解决方案,请留言让其他人知道。