Tout d'abord, vous ne devez utiliser aucun objet de domaine dans vos vues. Vous devriez utiliser des modèles de vue. Chaque modèle de vue contiendra uniquement les propriétés requises par la vue donnée ainsi que les attributs de validation spécifiques à cette vue donnée. Donc, si vous avez un assistant en 3 étapes, cela signifie que vous aurez 3 modèles de vue, un pour chaque étape:
public class Step1ViewModel
{
[Required]
public string SomeProperty { get; set; }
...
}
public class Step2ViewModel
{
[Required]
public string SomeOtherProperty { get; set; }
...
}
etc. Tous ces modèles de vue peuvent être soutenus par un modèle de vue principal de l'assistant:
public class WizardViewModel
{
public Step1ViewModel Step1 { get; set; }
public Step2ViewModel Step2 { get; set; }
...
}
alors vous pourriez avoir des actions de contrôleur rendant chaque étape du processus de l'assistant et passant le principal WizardViewModel
à la vue. Lorsque vous êtes à la première étape de l'action du contrôleur, vous pouvez initialiser la Step1
propriété. Ensuite, dans la vue, vous générez le formulaire permettant à l'utilisateur de remplir les propriétés de l'étape 1. Lorsque le formulaire est soumis, l'action du contrôleur appliquera les règles de validation pour l'étape 1 uniquement:
[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
var model = new WizardViewModel
{
Step1 = step1
};
if (!ModelState.IsValid)
{
return View(model);
}
return View("Step2", model);
}
Maintenant, dans la vue de l'étape 2, vous pouvez utiliser l' assistant Html.Serialize des futurs MVC afin de sérialiser l'étape 1 dans un champ caché à l'intérieur du formulaire (sorte de ViewState si vous le souhaitez):
@using (Html.BeginForm("Step2", "Wizard"))
{
@Html.Serialize("Step1", Model.Step1)
@Html.EditorFor(x => x.Step2)
...
}
et à l'intérieur de l'action POST de l'étape 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);
}
Et ainsi de suite jusqu'à ce que vous arriviez à la dernière étape où vous aurez WizardViewModel
rempli toutes les données. Ensuite, vous allez mapper le modèle de vue sur votre modèle de domaine et le transmettre à la couche de service pour traitement. La couche de service peut exécuter toutes les règles de validation elle-même et ainsi de suite ...
Il existe également une autre alternative: utiliser javascript et mettre tout sur la même page. Il existe de nombreux plugins jquery qui fournissent des fonctionnalités d'assistant ( Stepy est un bon). Il s'agit essentiellement d'afficher et de masquer les divs sur le client, auquel cas vous n'avez plus à vous soucier de l'état persistant entre les étapes.
Mais quelle que soit la solution que vous choisissez, utilisez toujours des modèles de vue et effectuez la validation sur ces modèles de vue. Tant que vous collerez des attributs de validation d'annotation de données sur vos modèles de domaine, vous aurez beaucoup de mal car les modèles de domaine ne sont pas adaptés aux vues.
METTRE À JOUR:
OK, en raison des nombreux commentaires, je tire la conclusion que ma réponse n'était pas claire. Et je dois être d'accord. Alors laissez-moi essayer de développer davantage mon exemple.
Nous pourrions définir une interface que tous les modèles de vue pas à pas devraient implémenter (c'est juste une interface de marqueur):
public interface IStepViewModel
{
}
puis nous définirions 3 étapes pour l'assistant où chaque étape ne contiendrait bien sûr que les propriétés dont elle a besoin ainsi que les attributs de validation pertinents:
[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; }
}
Ensuite, nous définissons le modèle de vue de l'assistant principal qui se compose d'une liste d'étapes et d'un index d'étape actuel:
[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();
}
}
Ensuite, nous passons au contrôleur:
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);
}
}
Quelques remarques sur ce contrôleur:
- L'action Index POST utilise les
[Deserialize]
attributs de la bibliothèque Microsoft Futures afin de vous assurer que vous avez installé MvcContrib
NuGet. C'est la raison pour laquelle les modèles de vue doivent être décorés avec l' [Serializable]
attribut
- L'action Index POST prend comme argument une
IStepViewModel
interface, donc pour que cela ait un sens, nous avons besoin d'un classeur de modèles personnalisé.
Voici le classeur de modèles associé:
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;
}
}
Ce classeur utilise un champ caché spécial appelé StepType qui contiendra le type concret de chaque étape et que nous enverrons à chaque demande.
Ce modèle de classeur sera enregistré dans Application_Start
:
ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());
Le dernier élément manquant du puzzle sont les vues. Voici la ~/Views/Wizard/Index.cshtml
vue principale :
@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" />
}
}
Et c'est tout ce dont vous avez besoin pour que cela fonctionne. Bien sûr, si vous le souhaitez, vous pouvez personnaliser l'apparence de certaines ou de toutes les étapes de l'assistant en définissant un modèle d'éditeur personnalisé. Par exemple, faisons-le pour l'étape 2. Nous définissons donc un ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml
partiel:
@model Step2ViewModel
Special Step 2
@Html.TextBoxFor(x => x.Bar)
Voici à quoi ressemble la structure:

Bien entendu, il y a place à amélioration. L'action Index POST ressemble à s..t. Il y a trop de code dedans. Une simplification supplémentaire impliquerait de déplacer tous les éléments de l'infrastructure comme l'index, la gestion actuelle des index, la copie de l'étape actuelle dans l'assistant, ... dans un autre classeur de modèles. Alors que finalement nous nous retrouvons avec:
[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);
}
c'est plutôt à quoi devraient ressembler les actions POST. Je laisse cette amélioration pour la prochaine fois :-)