Po pierwsze, nie powinieneś używać żadnych obiektów domeny w swoich widokach. Powinieneś używać modeli widoku. Każdy model widoku będzie zawierał tylko właściwości wymagane przez dany widok, a także atrybuty walidacji specyficzne dla tego danego widoku. Więc jeśli masz kreatora 3 kroków, oznacza to, że będziesz mieć 3 modele widoków, po jednym dla każdego kroku:
public class Step1ViewModel
{
[Required]
public string SomeProperty { get; set; }
...
}
public class Step2ViewModel
{
[Required]
public string SomeOtherProperty { get; set; }
...
}
i tak dalej. Wszystkie te modele widoków mogą być obsługiwane przez główny model widoku kreatora:
public class WizardViewModel
{
public Step1ViewModel Step1 { get; set; }
public Step2ViewModel Step2 { get; set; }
...
}
wtedy możesz mieć akcje kontrolera renderujące każdy krok procesu kreatora i przekazujące element main WizardViewModel
do widoku. Gdy jesteś na pierwszym kroku wewnątrz akcji kontrolera, możesz zainicjować Step1
właściwość. Następnie w widoku można wygenerować formularz umożliwiający użytkownikowi wypełnienie właściwości dotyczących kroku 1. Po przesłaniu formularza akcja kontrolera zastosuje reguły walidacji tylko dla kroku 1:
[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
var model = new WizardViewModel
{
Step1 = step1
};
if (!ModelState.IsValid)
{
return View(model);
}
return View("Step2", model);
}
Teraz w widoku kroku 2 możesz użyć pomocnika Html.Serialize z kontraktów terminowych MVC w celu serializacji kroku 1 do ukrytego pola w formularzu (rodzaj ViewState, jeśli chcesz):
@using (Html.BeginForm("Step2", "Wizard"))
{
@Html.Serialize("Step1", Model.Step1)
@Html.EditorFor(x => x.Step2)
...
}
i wewnątrz akcji POST kroku 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);
}
I tak dalej, aż dojdziesz do ostatniego kroku, w którym będziesz miał WizardViewModel
wypełnione wszystkie dane. Następnie zmapujesz model widoku na model domeny i przekażesz go do warstwy usług w celu przetworzenia. Warstwa usług może sama wykonywać dowolne reguły walidacji i tak dalej ...
Jest też inna alternatywa: użycie javascript i umieszczenie wszystkiego na tej samej stronie. Istnieje wiele wtyczek jquery , które zapewniają funkcjonalność kreatora ( Stepy jest fajny). Zasadniczo jest to kwestia wyświetlania i ukrywania elementów div na kliencie, w którym to przypadku nie musisz już martwić się o utrzymywanie się stanu między krokami.
Ale bez względu na to, jakie rozwiązanie wybierzesz, zawsze używaj modeli widoków i przeprowadzaj walidację na tych modelach widoków. Dopóki trzymasz atrybuty walidacji adnotacji danych w modelach domeny, będziesz mieć bardzo duże problemy, ponieważ modele domeny nie są przystosowane do widoków.
AKTUALIZACJA:
OK, z uwagi na liczne komentarze dochodzę do wniosku, że moja odpowiedź nie była jasna. I muszę się zgodzić. Spróbuję więc dalej rozwinąć mój przykład.
Moglibyśmy zdefiniować interfejs, który powinny implementować wszystkie modele widoków krokowych (to tylko interfejs znaczników):
public interface IStepViewModel
{
}
następnie zdefiniowalibyśmy 3 kroki dla kreatora, gdzie każdy krok zawierałby oczywiście tylko te właściwości, których wymaga, jak również odpowiednie atrybuty walidacji:
[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; }
}
następnie definiujemy główny model widoku kreatora, który składa się z listy kroków i aktualnego indeksu kroków:
[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();
}
}
Następnie przechodzimy do kontrolera:
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);
}
}
Kilka uwag na temat tego kontrolera:
- Akcja Index POST używa
[Deserialize]
atrybutów z biblioteki Microsoft Futures, więc upewnij się, że zainstalowano MvcContrib
pakiet NuGet. Z tego powodu modele widoków powinny być ozdobione [Serializable]
atrybutem
- Akcja Index POST przyjmuje jako argument
IStepViewModel
interfejs, więc aby miało to sens, potrzebujemy niestandardowego spinacza modelu.
Oto powiązany segregator modelu:
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;
}
}
Ten segregator używa specjalnego ukrytego pola o nazwie StepType, które będzie zawierać konkretny typ każdego kroku i które wyślemy na każde żądanie.
Ten model segregatora zostanie zarejestrowany w Application_Start
:
ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());
Ostatnim brakującym elementem układanki są widoki. Oto główny ~/Views/Wizard/Index.cshtml
widok:
@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" />
}
}
I to wszystko, czego potrzebujesz, aby to działało. Oczywiście, jeśli chcesz, możesz spersonalizować wygląd i działanie niektórych lub wszystkich kroków kreatora, definiując niestandardowy szablon edytora. Na przykład zróbmy to dla kroku 2. Więc definiujemy ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml
częściową:
@model Step2ViewModel
Special Step 2
@Html.TextBoxFor(x => x.Bar)
Oto jak wygląda struktura:
Oczywiście jest miejsce na ulepszenia. Akcja Index POST wygląda następująco: s..t. Jest w nim za dużo kodu. Dalsze uproszczenie polegałoby na przeniesieniu wszystkich elementów infrastruktury, takich jak indeks, zarządzanie bieżącym indeksem, kopiowanie bieżącego kroku do kreatora… do innego segregatora modelu. Więc w końcu otrzymujemy:
[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);
}
czyli bardziej jak powinny wyglądać akcje POST. Zostawiam tę poprawę na następny raz :-)