problemas de proceso de registro de varios pasos en asp.net mvc (modelos de vista divididos, modelo único)

Tengo un proceso de registro de varios pasos , respaldado por un único objeto en la capa de dominio , que tienen reglas de validación definidas en las propiedades.

¿Cómo debo validar el objeto de dominio cuando el dominio está dividido en muchas vistas, y tengo que guardar el objeto parcialmente en la primera vista cuando se publica?

Pensé en usar Sessions, pero eso no es posible porque el proceso es largo y la cantidad de datos es alta, así que no quiero usar la sesión.

Pensé en guardar todos los datos en un db relacional en memoria (con el mismo esquema que el db principal) y luego enjuagar esos datos en el db principal, pero surgieron problemas porque debería enrutar entre los servicios (solicitados en las vistas) que trabajan con el db principal y db en memoria.

Estoy buscando una solución elegante y limpia (más precisamente una mejor práctica).

ACTUALIZACIÓN Y aclaración:

@Darin Gracias por su respuesta, eso fue exactamente lo que hice hasta ahora. Pero, por cierto, tengo una solicitud que tiene muchos archivos adjuntos, diseño un Step2View por ejemplo, qué usuario puede subir documentos de forma asincrónica, pero esos archivos adjuntos deben guardarse en una tabla con relación referencial a otra tabla que debería haberse guardado antes en Step1View .

Por lo tanto, debo guardar el objeto de dominio en el paso Step1 (parcialmente), pero no puedo, porque el objeto de dominio principal respaldado que está asignado parcialmente al ViewModel de un Step1 no puede guardarse sin los accesorios que provienen de Step2ViewModel convertido.

Primero, no debería usar ningún objeto de dominio en sus vistas. Deberías estar usando modelos de vista. Cada modelo de vista contendrá solo las propiedades requeridas por la vista dada, así como los atributos de validación específicos de esta vista determinada. Entonces, si tiene un asistente de 3 pasos, esto significa que tendrá 3 modelos de vista, uno para cada paso:

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

y así. Todos los modelos de vista podrían estar respaldados por un modelo de vista de asistente principal:

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

entonces podría tener acciones de control que representen cada paso del proceso del asistente y pasando el WizardViewModel principal a la vista. Cuando está en el primer paso dentro de la acción del controlador, puede inicializar la propiedad Step1 . Luego, dentro de la vista generaría el formulario que permite al usuario completar las propiedades del paso 1. Cuando se envíe el formulario, la acción del controlador aplicará las reglas de validación para el paso 1 solamente:

 [HttpPost] public ActionResult Step1(Step1ViewModel step1) { var model = new WizardViewModel { Step1 = step1 }; if (!ModelState.IsValid) { return View(model); } return View("Step2", model); } 

Ahora, dentro de la vista del paso 2, podría usar el helper Html.Serialize de futuros de MVC para serializar el paso 1 en un campo oculto dentro del formulario (una especie de ViewState si lo desea):

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

y dentro de la acción POST del paso 2:

 [HttpPost] public ActionResult Step2(Step2ViewModel step2, [Deserialize] Step1ViewModel step1) { var model = new WizardViewModel { Step1 = step1, Step2 = step2 } if (!ModelState.IsValid) { return View(model); } return View("Step3", model); } 

Y así sucesivamente hasta llegar al último paso donde tendrá el WizardViewModel lleno de todos los datos. A continuación, asignará el modelo de vista a su modelo de dominio y lo pasará a la capa de servicio para su procesamiento. La capa de servicio puede realizar las reglas de validación en sí y así sucesivamente …

También hay otra alternativa: usar javascript y poner todo en la misma página. Hay muchos complementos de jquery que proporcionan funcionalidad de asistente ( Stepy es agradable). Básicamente se trata de mostrar y ocultar divs en el cliente, en cuyo caso ya no tendrá que preocuparse por el estado persistente entre los pasos.

Pero no importa qué solución elija, use siempre modelos de vista y realice la validación en esos modelos de vista. Mientras mantenga los atributos de validación de la anotación de datos en los modelos de su dominio, tendrá dificultades ya que los modelos de dominio no se adaptan a las vistas.


ACTUALIZAR:

OK, debido a los numerosos comentarios saco la conclusión de que mi respuesta no estaba clara. Y debo estar de acuerdo. Así que déjame tratar de seguir elaborando mi ejemplo.

Podríamos definir una interfaz que deberían implementar todos los modelos de vista por pasos (es solo una interfaz de marcador):

 public interface IStepViewModel { } 

entonces definiríamos 3 pasos para el asistente, donde cada paso, por supuesto, contendría solo las propiedades que requiere, así como los atributos de validación relevantes:

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

a continuación definimos el modelo de vista de asistente principal que consiste en una lista de pasos y un índice de paso actual:

 [Serializable] public class WizardViewModel { public int CurrentStepIndex { get; set; } public IList 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(); } } 

Luego pasamos al controlador:

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

Un par de comentarios sobre este controlador:

  • La acción Index POST usa los atributos [Deserialize] de la biblioteca Microsoft Futures, así que asegúrese de haber instalado MvcContrib NuGet. Esa es la razón por la que los modelos de visualización deben decorarse con el atributo [Serializable]
  • La acción Index POST toma como argumento una interfaz IStepViewModel , por lo que para que tenga sentido, necesitamos una carpeta de modelo personalizada.

Aquí está la carpeta de modelo asociada:

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

Esta carpeta utiliza un campo oculto especial llamado StepType que contendrá el tipo concreto de cada paso y que enviaremos en cada solicitud.

Esta carpeta modelo se registrará en Application_Start :

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

El último pedazo del rompecabezas que falta son las vistas. Aquí está la vista principal de ~/Views/Wizard/Index.cshtml :

 @using Microsoft.Web.Mvc @model WizardViewModel @{ var currentStep = Model.Steps[Model.CurrentStepIndex]; } 

Step @(Model.CurrentStepIndex + 1) out of @Model.Steps.Count

@using (Html.BeginForm()) { @Html.Serialize("wizard", Model) @Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType()) @Html.EditorFor(x => currentStep, null, "") if (Model.CurrentStepIndex > 0) { } if (Model.CurrentStepIndex < Model.Steps.Count - 1) { } else { } }

Y eso es todo lo que necesitas para que esto funcione. Por supuesto, si lo desea, puede personalizar la apariencia de algunos o todos los pasos del asistente definiendo una plantilla de editor personalizada. Por ejemplo, hagámoslo para el paso 2. Entonces definimos un ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml partial:

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

Así es como se ve la estructura:

enter image description here

Por supuesto, hay margen de mejora. La acción Index POST se parece a s..t. Hay demasiado código en él. Una simplificación adicional implicaría mover todo el material de la infraestructura como índice, gestión de índice actual, copia del paso actual en el asistente, … en otro archivador modelo. Para que finalmente terminemos con:

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

que es más cómo deberían verse las acciones POST. Dejo esta mejora para la próxima vez 🙂

Para complementar la respuesta de Amit Bagga, a continuación encontrará lo que hice. Incluso si es menos elegante, me resulta más sencillo que la respuesta de Darin.

Controlador :

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

Modelos:

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

Le sugiero que mantenga el estado de Proceso completo en el cliente utilizando Jquery.

Por ejemplo, tenemos un proceso de asistente de tres pasos.

  1. El usuario se presentó con el paso 1 en el que tiene un botón etiquetado como “Siguiente”
  2. Al hacer clic en Siguiente, realizamos una solicitud de Ajax y creamos un DIV llamado Step2 y cargamos el HTML en ese DIV.
  3. En el Paso 3 tenemos un botón con la etiqueta “Finalizado” al hacer clic en el botón publicar los datos usando la llamada $ .post.

De esta forma, puede crear fácilmente su objeto de dominio directamente a partir de los datos de entrada del formulario y, en caso de que los datos tengan errores, devolver un JSON válido que contenga todos los mensajes de error y mostrarlos en un div.

Por favor divida los Pasos

 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 es solo una demostración que te ayudará a lograr el resultado final. En el paso final, debe crear el objeto de dominio y rellenar los valores correctos del objeto del asistente y almacenarlos en la base de datos.

Los asistentes son simples pasos para procesar un modelo simple. No hay ninguna razón para crear múltiples modelos para un asistente. Todo lo que haría sería crear un único modelo y pasarlo entre las acciones en un solo controlador.

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

El conjunto de arriba es estúpido simple así que reemplace sus campos allí. A continuación, comenzamos con una acción simple que inicia nuestro asistente.

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

Esto llama a la vista “WizardStep1.cshtml” (si usa una afeitadora). Puede usar el asistente de creación de plantillas si lo desea. Simplemente redirigiremos la publicación a una acción diferente.

  @using (Html.BeginForm("WizardStep2", "MyWizard")) { 

Lo importante es que publicaremos esto en una acción diferente; la acción WizardStep2

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

En esta acción, verificamos si nuestro modelo es válido y, de ser así, lo enviamos a nuestra vista WizardStep2.cshtml; de lo contrario, lo enviaremos al paso uno con los errores de validación. En cada paso lo enviamos al siguiente paso, validamos ese paso y seguimos adelante. Ahora, algunos desarrolladores inteligentes podrían decir que no podemos avanzar entre pasos como este si utilizamos los atributos [Requerido] u otras anotaciones de datos entre los pasos. Y estarías en lo cierto, así que elimina los errores en los elementos que aún no se han verificado. como abajo.

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

Finalmente, guardaríamos el modelo una vez en el almacén de datos. Esto también evita que un usuario que inicia un asistente pero no lo termina no guarde datos incompletos en la base de datos.

Espero que este método de implementación de un asistente sea mucho más fácil de usar y mantener que cualquiera de los métodos mencionados anteriormente.

Gracias por leer.

Quería compartir mi propia forma de manejar estos requisitos. No quería usar SessionState en absoluto, ni tampoco quería que se manejara desde el lado del cliente, y el método serialize requiere MVC Futures, que no quería incluir en mi proyecto.

En su lugar, construí un HTML Helper que iterará a través de todas las propiedades del modelo y generará un elemento oculto personalizado para cada uno. Si es una propiedad compleja, se ejecutará de forma recursiva en ella.

En su forma, se publicarán en el controlador junto con los datos del nuevo modelo en cada paso del “asistente”.

Escribí esto para 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(this HtmlHelper html, Expression> expression) { return HiddenClassFor(html, expression, null); } public static MvcHtmlString HiddenClassFor(this HtmlHelper html, Expression> 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(HtmlHelper html, LambdaExpression expression, ModelMetadata metaData, IDictionary 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(this HtmlHelper html, Expression> expression) { return HiddenArrayFor(html, expression, null); } public static MvcHtmlString HiddenArrayFor(this HtmlHelper html, Expression> 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(HtmlHelper html, LambdaExpression expression, ModelMetadata metaData, IDictionary 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(this HtmlHelper html, Expression> expression) { return MinHiddenFor(html, expression, null); } public static MvcHtmlString MinHiddenFor(this HtmlHelper html, Expression> 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 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 _pair in htmlAttributes) { _input.MergeAttribute(_pair.Key, _pair.Value.ToString(), true); } } return new MvcHtmlString(_input.ToString(TagRenderMode.SelfClosing)); } } } 

Ahora, para todos los pasos de su “asistente”, puede usar el mismo modelo base y pasar las propiedades del modelo “Paso 1,2,3” al helper @ Html.HiddenClassFor utilizando una expresión lambda.

Incluso puede tener un botón de retroceso en cada paso si lo desea. Solo tiene un botón de retroceso en su formulario que lo publicará en una acción StepNBack en el controlador utilizando el atributo de formacion. No incluido en el ejemplo a continuación, sino solo una idea para ti.

De todos modos aquí hay un ejemplo básico:

Aquí está tu MODELO

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

Aquí está su CONTROLADOR

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

Aquí están tus VISTAS

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

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

Paso 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" }, });  } 

Agregando más información de la respuesta de @ Darin.

¿Qué sucede si tiene un estilo de diseño separado para cada paso y desea mantener cada uno en una vista parcial separada o qué sucede si tiene varias propiedades para cada paso?

Al usar Html.EditorFor tenemos limitaciones para usar la vista parcial.

Cree 3 vistas parciales en la carpeta Shared llamada: Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

Para abreviar, acabo de publicar la 1ra vista patial, otros pasos son los mismos que la respuesta de 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 

Personal Details

@Html.TextBoxFor(x => x.FirstName)
@Html.TextBoxFor(x => x.LastName)
@Html.TextBoxFor(x => x.PhoneNo)
@Html.TextBoxFor(x => x.EmailId)

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

Step @(Model.CurrentStepIndex + 1) out of @Model.Steps.Count

@using (Html.BeginForm()) { @Html.Serialize("wizard", Model) @Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType()) @Html.Partial(""+ viewName + "", currentStep); if (Model.CurrentStepIndex > 0) { } if (Model.CurrentStepIndex < Model.Steps.Count - 1) { } else { } }

Si hay alguna solución mejor, por favor coméntalo para que otros lo sepan.

Una opción es crear un conjunto de tablas idénticas que almacenarán los datos recostackdos en cada paso. Luego, en el último paso, si todo va bien, puede crear la entidad real copiando los datos temporales y almacenándolos.

Otro es crear Value Objects para cada paso y almacenarlos en Cache o Session . Entonces, si todo va bien, puede crear su objeto de dominio y guardarlo