ASP.NET MVC中的多步骤注册过程问题(拆分视图模型,单个模型)


117

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

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

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

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

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

更新和说明:

@Darin谢谢您的深思熟虑,这就是我到目前为止所做的。但是顺便说一句,我有一个包含许多附件的请求,我设计了一个Step2View示例,例如,用户可以异步上载其中的文档,但是这些附件应保存在一个表中,该表应与之前应保存的另一个表建立引用关系。Step1View

因此,我应该将域对象保存在Step1(部分)中,但是,我不能这样做,因为如果没有来自convert的道具,则无法保存部分映射到Step1的ViewModel的支持的Core Domain对象Step2ViewModel


@Jani,你有没有发现这个上传的片段?我想动动你的大脑。我正在处理这个确切的问题。
道格·张伯伦

1
博客中的解决方案非常简单直接。它通过分散div的可见性和不显眼的jquery验证,将div用作“步骤”。
德米特里·埃菲缅科

Answers:


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步视图中,您可以使用MVC期货中的Html.Serialize帮助程序,以将第1步序列化为表单内的隐藏字段(如果需要,可以排序为ViewState):

@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插件Stepy是一个不错的插件)。基本上,这是在客户端上显示和隐藏div的问题,在这种情况下,您不再需要担心步骤之间的持久状态。

但是,无论您选择哪种解决方案,都始终使用视图模型并对这些视图模型执行验证。只要您在域模型上粘贴数据注释验证属性,由于域模型不适合视图,您将很难解决。


更新:

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

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

public interface IStepViewModel
{
}

那么我们将为向导定义3个步骤,其中每个步骤当然将仅包含其所需的属性以及相关的验证属性:

[Serializable]
public class Step1ViewModel: IStepViewModel
{
    [Required]
    public string Foo { get; set; }
}

[Serializable]
public class Step2ViewModel : IStepViewModel
{
    public string Bar { get; set; }
}

[Serializable]
public class Step3ViewModel : IStepViewModel
{
    [Required]
    public string Baz { get; set; }
}

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

[Serializable]
public class WizardViewModel
{
    public int CurrentStepIndex { get; set; }
    public IList<IStepViewModel> Steps { get; set; }

    public void Initialize()
    {
        Steps = typeof(IStepViewModel)
            .Assembly
            .GetTypes()
            .Where(t => !t.IsAbstract && typeof(IStepViewModel).IsAssignableFrom(t))
            .Select(t => (IStepViewModel)Activator.CreateInstance(t))
            .ToList();
    }
}

然后我们转到控制器:

public class WizardController : Controller
{
    public ActionResult Index()
    {
        var wizard = new WizardViewModel();
        wizard.Initialize();
        return View(wizard);
    }

    [HttpPost]
    public ActionResult Index(
        [Deserialize] WizardViewModel wizard, 
        IStepViewModel step
    )
    {
        wizard.Steps[wizard.CurrentStepIndex] = step;
        if (ModelState.IsValid)
        {
            if (!string.IsNullOrEmpty(Request["next"]))
            {
                wizard.CurrentStepIndex++;
            }
            else if (!string.IsNullOrEmpty(Request["prev"]))
            {
                wizard.CurrentStepIndex--;
            }
            else
            {
                // TODO: we have finished: all the step partial
                // view models have passed validation => map them
                // back to the domain model and do some processing with
                // the results

                return Content("thanks for filling this form", "text/plain");
            }
        }
        else if (!string.IsNullOrEmpty(Request["prev"]))
        {
            // Even if validation failed we allow the user to
            // navigate to previous steps
            wizard.CurrentStepIndex--;
        }
        return View(wizard);
    }
}

关于此控制器的几点说明:

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

这是关联的模型联编程序:

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

该绑定器使用一个称为StepType的特殊隐藏字段,该字段将包含每个步骤的具体类型,并且我们将根据每个请求发送该字段。

该型号资料夹将在以下位置注册Application_Start

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

难题中最后遗漏的是视图。这是主要~/Views/Wizard/Index.cshtml视图:

@using Microsoft.Web.Mvc
@model WizardViewModel

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

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

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

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

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

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

这就是您要做的所有事情。当然,如果您愿意,可以通过定义自定义编辑器模板来个性化向导的某些或所有步骤的外观。例如,让我们在第2步中进行操作。因此,我们定义了一个~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml局部变量:

@model Step2ViewModel

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

结构如下:

在此处输入图片说明

当然还有改进的空间。Index POST操作看起来像s..t。其中包含太多代码。进一步的简化将涉及到移动所有基础架构内容,例如索引,当前索引管理,将当前步骤复制到向导中,...到另一个模型绑定器中。这样最终我们得到:

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

这就是POST操作的外观。我将在下一次离开此改进:-)


1
@Doug Chamberlain,我使用AutoMapper在视图模型和域模型之间进行转换。
Darin Dimitrov

1
@Doug Chamberlain,请参阅我的最新答案。我希望它使事情比我最初的帖子更清晰。
Darin Dimitrov

20
+1 @Jani:您真的需要给达琳50分才能得到这个答案。非常全面。而且他设法重申了使用ViewModel而不是Domain模型的必要;-)
Tom Chantler

3
我在任何地方都找不到Deserialize属性...同样在mvccontrib的codeplex页面中,我找到了这个94fa6078a115 by Jeremy Skinner 2010年8月1日下午5:55 0删除不赞成使用的Deserialize活页夹您建议我做什么?
查克·诺里斯

2
我发现了一个问题,但没有命名视图Step1,Step2等。我的名字被命名为更有意义的名称,但不是按字母顺序排列。因此,我最终将模型弄错了顺序。我向IStepViewModel接口添加了StepNumber属性。现在,我可以在WizardViewModel的Initialize方法中对此进行排序。
杰夫雷迪2012年

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

11

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

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

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

这样,您可以轻松地直接从表单发布数据中构建域对象,并且如果数据有错误,则返回保存所有错误消息的有效JSON并将它们显示在div中。

请分步骤

public class Wizard 
{
  public Step1 Step1 {get;set;}
  public Step2 Step2 {get;set;}
  public Step3 Step3 {get;set;}
}

public ActionResult Step1(Step1 step)
{
  if(Model.IsValid)
 {
   Wizard wiz = new Wizard();
   wiz.Step1 = step;
  //Store the Wizard in Session;
  //Return the action
 }
}

public ActionResult Step2(Step2 step)
{
 if(Model.IsValid)
 {
   //Pull the Wizard From Session
   wiz.Step2=step;
 }
}

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


是的,这是一个有趣的解决方案,但是很遗憾,客户端的互联网连接很差,他/她应该向我们发送一堆文件。所以我们早些时候拒绝了该解决方案。
贾汉(Jahan)

您能否让我知道客户端要上传的数据量。
阿米特·巴加

几个文件,将近十个,每个文件将近1 MB。
贾汉(Jahan)

5

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

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

上面的代码很愚蠢,因此请替换其中的字段。接下来,我们从启动向导的简单操作开始。

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

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

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

需要注意的是,我们将把它发布到另一个动作上。WizardStep2操作

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

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

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

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

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

谢谢阅读。


您有完整的解决方案可以尝试吗?谢谢
mpora

5

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

相反,我构建了一个HTML 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);
    }
}

这是您的意见

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

第三步

@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我们有局限性,无法使用局部视图。

Shared名为的文件夹下创建3个局部视图: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" />

    }
}

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


-9

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

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


1
如果投反对票的人也给出自己的理由,那就太好了。
马丁

并没有否决您的意见,但是您的答案与问题完全无关。OP询问如何创建向导,同时在后面答复如何处理响应。
Dementic 2015年

1
我通常不投票,但是当我投票时,我要确保投票赞成:-)
Suhail Mumtaz Awan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.