asp.net mvc中的多步注册流程问题(split viewmodels,single model)

时间:2011-06-19 13:31:19

标签: asp.net-mvc

我有多步注册流程,由域图层中的单个对象支持,其中包含在属性上定义的验证规则。

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

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

我考虑过保存关系内存数据库中的所有数据(使用与主数据库相同的模式),然后将数据刷新到主数据库,但出现问题导致我应该在服务之间路由(在视图中请求)谁使用主数据库和内存数据库。

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

更新和澄清:

@Darin感谢您的深思熟虑, 这正是我到目前为止所做的。 但顺便提一下,我有一个请求,里面有很多附件,我设计了Step2View例如哪个用户可以异步上传文档, 但是这些附件应保存在一个表格中,该表格与Step1View之前应保存的另一个表格具有参考关系。

因此我应该在Step1(部分)中保存域对象,但我不能, 导致部分映射到Step1的ViewModel的支持的Core Domain对象无法在没有转换Step2ViewModel的道具的情况下保存。

7 个答案:

答案 0 :(得分:224)

首先,您不应在视图中使用任何域对象。您应该使用视图模型。每个视图模型将仅包含给定视图所需的属性以及特定于此给定视图的验证属性。因此,如果您有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步视图中,您可以使用MVC期货中的Html.Serialize helper,以便将第1步序列化为表单内的隐藏字段(如果您愿意,可以将ViewState排序):

@using (Html.BeginForm("Step2", "Wizard"))
{
    @Html.Serialize("Step1", Model.Step1)
    @Html.EditorFor(x => x.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);
}

依此类推,直到你完成最后一步,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);
    }
}

关于这个控制器的几点评论:

  • Index POST操作使用Microsoft Futures库中的[Deserialize]属性,因此请确保已安装MvcContrib NuGet。这就是为什么视图模型应该使用[Serializable]属性
  • 进行修饰的原因
  • Index POST操作将IStepViewModel接口作为参数,因此为此我们需要一个自定义模型绑定器。

这是相关的模型绑定器:

public class StepViewModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var stepTypeValue = bindingContext.ValueProvider.GetValue("StepType");
        var stepType = Type.GetType((string)stepTypeValue.ConvertTo(typeof(string)), true);
        var step = Activator.CreateInstance(stepType);
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => step, stepType);
        return step;
    }
}

此绑定器使用一个名为StepType的特殊隐藏字段,该字段将包含每个步骤的具体类型,并将在每个请求中发送。

此模型活页夹将在Application_Start

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

难题的最后一点是观点。这是主~/Views/Wizard/Index.cshtml视图:

@using Microsoft.Web.Mvc
@model WizardViewModel

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

<h3>Step @(Model.CurrentStepIndex + 1) out of @Model.Steps.Count</h3>

@using (Html.BeginForm())
{
    @Html.Serialize("wizard", Model)

    @Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType())
    @Html.EditorFor(x => currentStep, null, "")

    if (Model.CurrentStepIndex > 0)
    {
        <input type="submit" value="Previous" name="prev" />
    }

    if (Model.CurrentStepIndex < Model.Steps.Count - 1)
    {
        <input type="submit" value="Next" name="next" />
    }
    else
    {
        <input type="submit" value="Finish" name="finish" />
    }
}

这就是你需要做的所有工作。当然,如果您希望通过定义自定义编辑器模板,可以个性化向导的部分或全部步骤的外观。例如,让我们为第2步执行此操作。因此,我们定义~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml部分:

@model Step2ViewModel

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

以下是结构的样子:

enter image description here

当然还有改进的余地。 Index POST操作看起来像s..t。它中的代码太多了。进一步的简化将涉及移动所有基础设施的东西,如索引,当前索引管理,将当前步骤复制到向导,...到另一个模型绑定器。最后我们最终得到:

[HttpPost]
public ActionResult Index(WizardViewModel wizard)
{
    if (ModelState.IsValid)
    {
        // TODO: we have finished: all the step partial
        // view models have passed validation => map them
        // back to the domain model and do some processing with
        // the results
        return Content("thanks for filling this form", "text/plain");
    }
    return View(wizard);
}

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

答案 1 :(得分:13)

为了补充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; }
    }

答案 2 :(得分:10)

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

  

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

  1. 用户出现了Step1,其上有一个标签为“Next”
  2. 的按钮
  3. On Clicking Next我们制作一个Ajax请求并创建一个名为Step2的DIV并将HTML加载到该DIV中。
  4. 在Step3上,我们有一个标有“已完成”的按钮,点击按钮,使用$ .post调用发布数据。
  5. 通过这种方式,您可以直接从表单发布数据轻松构建域对象,如果数据有错误,则返回有效的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;
     }
    }
    
      

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

答案 3 :(得分: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(如果使用razor)。如果需要,可以使用创建模板向导。我们只是将帖子重定向到其他操作。

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

值得注意的是,我们将把这个发布到另一个动作; WizardStep2动作

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

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

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

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

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

感谢阅读。

答案 4 :(得分:4)

我想分享自己处理这些要求的方式。我根本不想使用SessionState,也不想让它处理客户端,而serialize方法需要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));
        }
    }
}

现在,对于“向导”的所有步骤,您可以使用相同的基本模型,并使用lambda表达式将“Step 1,2,3”模型属性传递到@ Html.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);
    }
}

以下是您的VIEWS

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

答案 5 :(得分:1)

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

如果您为每个步骤设置了单独的设计风格,并希望将每个步骤保存在单独的局部视图中,或者如果每个步骤有多个属性,该怎么办?

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

在名为Shared

Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml文件夹下创建3个部分视图

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

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

    }
}

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

答案 6 :(得分:-9)

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

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