Zunächst sollten Sie in Ihren Ansichten keine Domänenobjekte verwenden. Sie sollten Ansichtsmodelle verwenden. Jedes Ansichtsmodell enthält nur die Eigenschaften, die für die angegebene Ansicht erforderlich sind, sowie die für diese bestimmte Ansicht spezifischen Validierungsattribute. Wenn Sie also einen Assistenten für drei Schritte haben, bedeutet dies, dass Sie drei Ansichtsmodelle haben, eines für jeden Schritt:
public class Step1ViewModel
{
[Required]
public string SomeProperty { get; set; }
...
}
public class Step2ViewModel
{
[Required]
public string SomeOtherProperty { get; set; }
...
}
und so weiter. Alle diese Ansichtsmodelle können von einem Hauptansichtsmodell des Assistenten unterstützt werden:
public class WizardViewModel
{
public Step1ViewModel Step1 { get; set; }
public Step2ViewModel Step2 { get; set; }
...
}
Dann könnten Sie Controller-Aktionen ausführen, die jeden Schritt des Assistentenprozesses rendern und den Hauptschritt WizardViewModelan die Ansicht übergeben. Wenn Sie sich im ersten Schritt der Controller-Aktion befinden, können Sie die Step1Eigenschaft initialisieren . Anschließend generieren Sie in der Ansicht das Formular, mit dem der Benutzer die Eigenschaften von Schritt 1 ausfüllen kann. Wenn das Formular gesendet wird, wendet die Controller-Aktion nur die Validierungsregeln für Schritt 1 an:
[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
var model = new WizardViewModel
{
Step1 = step1
};
if (!ModelState.IsValid)
{
return View(model);
}
return View("Step2", model);
}
In der Ansicht von Schritt 2 können Sie jetzt den Html.Serialize-Helfer aus MVC-Futures verwenden, um Schritt 1 in ein verstecktes Feld innerhalb des Formulars zu serialisieren (eine Art ViewState, wenn Sie dies wünschen):
@using (Html.BeginForm("Step2", "Wizard"))
{
@Html.Serialize("Step1", Model.Step1)
@Html.EditorFor(x => x.Step2)
...
}
und innerhalb der POST-Aktion von Schritt 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);
}
Und so weiter, bis Sie zum letzten Schritt gelangen, in dem Sie WizardViewModelalle Daten ausgefüllt haben . Anschließend ordnen Sie das Ansichtsmodell Ihrem Domänenmodell zu und übergeben es zur Verarbeitung an die Serviceschicht. Die Serviceschicht führt möglicherweise alle Validierungsregeln selbst aus und so weiter ...
Es gibt auch eine andere Alternative: Verwenden Sie Javascript und setzen Sie alle auf dieselbe Seite. Es gibt viele JQuery-Plugins , die Assistentenfunktionen bieten ( Stepy ist eine nette). Im Grunde geht es darum, Divs auf dem Client anzuzeigen und auszublenden. In diesem Fall müssen Sie sich keine Sorgen mehr über den anhaltenden Zustand zwischen den Schritten machen.
Unabhängig davon, für welche Lösung Sie sich entscheiden, verwenden Sie immer Ansichtsmodelle und führen Sie die Validierung für diese Ansichtsmodelle durch. Solange Sie die Validierungsattribute für Datenanmerkungen in Ihre Domänenmodelle einfügen, werden Sie große Probleme haben, da Domänenmodelle nicht an Ansichten angepasst sind.
AKTUALISIEREN:
OK, aufgrund der zahlreichen Kommentare komme ich zu dem Schluss, dass meine Antwort nicht klar war. Und ich muss zustimmen. Lassen Sie mich versuchen, mein Beispiel weiter auszuarbeiten.
Wir könnten eine Schnittstelle definieren, die alle Schrittansichtsmodelle implementieren sollten (es ist nur eine Markierungsschnittstelle):
public interface IStepViewModel
{
}
Dann würden wir 3 Schritte für den Assistenten definieren, wobei jeder Schritt natürlich nur die Eigenschaften enthält, die er benötigt, sowie die relevanten Validierungsattribute:
[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; }
}
Als nächstes definieren wir das Hauptansichtsmodell des Assistenten, das aus einer Liste von Schritten und einem aktuellen Schrittindex besteht:
[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();
}
}
Dann gehen wir weiter zum Controller:
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);
}
}
Einige Anmerkungen zu diesem Controller:
- Die Aktion "Index-POST" verwendet die
[Deserialize]Attribute aus der Microsoft Futures-Bibliothek. Stellen Sie daher sicher, dass Sie MvcContribNuGet installiert haben . Aus diesem Grund sollten Ansichtsmodelle mit dem [Serializable]Attribut versehen werden
- Die Index-POST-Aktion verwendet als Argument eine
IStepViewModelSchnittstelle. Damit dies sinnvoll ist, benötigen wir einen benutzerdefinierten Modellordner.
Hier ist der zugehörige Modellordner:
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;
}
}
Dieser Ordner verwendet ein spezielles verstecktes Feld namens StepType, das den konkreten Typ jedes Schritts enthält und das wir bei jeder Anfrage senden.
Dieser Modellordner wird registriert in Application_Start:
ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());
Das letzte fehlende Teil des Puzzles sind die Ansichten. Hier ist die Hauptansicht ~/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" />
}
}
Und das ist alles, was Sie brauchen, damit dies funktioniert. Wenn Sie möchten, können Sie natürlich das Erscheinungsbild einiger oder aller Schritte des Assistenten personalisieren, indem Sie eine benutzerdefinierte Editorvorlage definieren. Machen wir es zum Beispiel für Schritt 2. Also definieren wir einen ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtmlTeil:
@model Step2ViewModel
Special Step 2
@Html.TextBoxFor(x => x.Bar)
So sieht die Struktur aus:

Natürlich gibt es Raum für Verbesserungen. Die Aktion Index POST sieht aus wie s..t. Es ist zu viel Code darin. Eine weitere Vereinfachung würde darin bestehen, alle Infrastrukturelemente wie Index, aktuelle Indexverwaltung, Kopieren des aktuellen Schritts in den Assistenten usw. in einen anderen Modellordner zu verschieben. Damit wir endlich am Ende haben:
[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);
}
So sollten POST-Aktionen aussehen. Ich verlasse diese Verbesserung für das nächste Mal :-)