Primero, no debería utilizar ningún objeto de dominio en sus vistas. Debería utilizar 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. 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 esos modelos de vista podrían estar respaldados por un modelo de vista del asistente principal:
public class WizardViewModel
{
public Step1ViewModel Step1 { get; set; }
public Step2ViewModel Step2 { get; set; }
...
}
entonces podría tener acciones de controlador que representen cada paso del proceso del asistente y pasen el principal WizardViewModela la vista. Cuando esté en el primer paso dentro de la acción del controlador, puede inicializar la Step1propiedad. Luego, dentro de la vista, generaría el formulario que permite al usuario completar las propiedades sobre el paso 1. Cuando se envía el formulario, la acción del controlador aplicará las reglas de validación para el paso 1 únicamente:
[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, puede usar el ayudante Html.Serialize de futuros 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 que llegues al último paso donde tendrás el WizardViewModelrelleno con todos los datos. Luego, mapeará el modelo de vista a su modelo de dominio y lo pasará a la capa de servicio para su procesamiento. La capa de servicio podría realizar cualquier regla de validación por sí misma, etc.
También hay otra alternativa: usar javascript y poner todo en la misma página. Hay muchos complementos de jquery que brindan funcionalidad de asistente ( Stepy es uno bueno). 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, utilice siempre modelos de vista y realice la validación en esos modelos de vista. Mientras pegue atributos de validación de anotaciones de datos en sus modelos de dominio, tendrá muchas dificultades, ya que los modelos de dominio no se adaptan a las vistas.
ACTUALIZAR:
De acuerdo, debido a los numerosos comentarios, saco la conclusión de que mi respuesta no fue clara. Y debo estar de acuerdo. Permítanme intentar desarrollar más mi ejemplo.
Podríamos definir una interfaz que deberían implementar todos los modelos de vista de pasos (es solo una interfaz de marcador):
public interface IStepViewModel
{
}
luego 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 del asistente principal, que consta de una lista de pasos y un índice de pasos actual:
[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();
}
}
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
[Deserialize]atributos de la biblioteca Microsoft Futures, así que asegúrese de haber instalado MvcContribNuGet. Esa es la razón por la que los modelos de vista deben decorarse con el[Serializable] atributo
- La acción Index POST toma como argumento una
IStepViewModelinterfaz, por lo que para que esto tenga sentido, necesitamos una carpeta de modelos personalizada.
Aquí está la carpeta de modelos 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.
Este modelo de carpeta quedará registrado en Application_Start:
ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());
La última parte del rompecabezas que falta son las vistas. Aquí está la ~/Views/Wizard/Index.cshtmlvista principal :
@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" />
}
}
Y eso es todo lo que necesita 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.cshtmlparcial:
@model Step2ViewModel
Special Step 2
@Html.TextBoxFor(x => x.Bar)
Así es como se ve la estructura:

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 todas las cosas de la infraestructura como el índice, la administración del índice actual, la copia del paso actual en el asistente, ... en otra carpeta de modelos. 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 :-)