По-перше, ви не повинні використовувати жодні об’єкти домену у своїх представленнях. Вам слід використовувати моделі перегляду. Кожна модель перегляду буде містити лише властивості, необхідні для даного представлення даних, а також атрибути перевірки, характерні для даного представлення даних. Отже, якщо у вас є майстер з 3 кроками, це означає, що у вас буде 3 моделі перегляду, по одній на кожен крок:
public class Step1ViewModel
{
[Required]
public string SomeProperty { get; set; }
...
}
public class Step2ViewModel
{
[Required]
public string SomeOtherProperty { get; set; }
...
}
і так далі. Усі ці моделі перегляду можуть бути підкріплені основною моделлю перегляду майстра:
public class WizardViewModel
{
public Step1ViewModel Step1 { get; set; }
public Step2ViewModel Step2 { get; set; }
...
}
тоді ви могли б WizardViewModel
виконати дії контролера, що рендерують кожен крок процесу майстра і передають головний на вигляд. Коли ви перебуваєте на першому кроці всередині дії контролера, ви можете ініціалізувати Step1
властивість. Тоді всередині подання ви б генерували форму, що дозволяє користувачеві заповнити властивості приблизно на етапі 1. Коли форма подається, дія контролера застосує правила перевірки лише для кроку 1:
[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
var model = new WizardViewModel
{
Step1 = step1
};
if (!ModelState.IsValid)
{
return View(model);
}
return View("Step2", model);
}
Тепер усередині етапу 2 ви можете використовувати Html.Serialize помічник з ф'ючерсів MVC, щоб серіалізувати крок 1 у приховане поле всередині форми (на зразок ViewState, якщо бажаєте):
@using (Html.BeginForm("Step2", "Wizard"))
{
@Html.Serialize("Step1", Model.Step1)
@Html.EditorFor(x => x.Step2)
...
}
і всередині дії POST кроку 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);
}
І так далі, поки ви не дістанетеся до останнього кроку, де вам належить WizardViewModel
заповнені всіма даними. Потім ви будете зіставити модель перегляду до вашої моделі домену та передати її на рівень обслуговування для обробки. Сервісний рівень може виконувати будь-які правила перевірки і так далі ...
Є й інша альтернатива: використання javascript та розміщення всіх на одній сторінці. Існує багато плагінів jquery , які забезпечують функціонування майстра ( Stepy - приємний). В основному справа в тому, щоб показати і приховати діви на клієнті, і в цьому випадку вам більше не потрібно турбуватися про збереження стану між кроками.
Але незалежно від того, яке рішення ви обрали, завжди використовуйте моделі перегляду та виконуйте перевірку на цих моделях перегляду. Поки ви наклеюєте атрибути перевірки анотації даних на ваших моделях домену, ви будете дуже важко боротися, оскільки доменні моделі не адаптовані до представлень.
ОНОВЛЕННЯ:
Гаразд, завдяки численним коментарям я роблю висновок, що моя відповідь була не однозначною. І я повинен погодитися. Тож дозвольте спробувати детальніше розробити свій приклад.
Ми могли б визначити інтерфейс, який повинні застосовувати всі крокові моделі перегляду (це лише інтерфейс маркера):
public interface IStepViewModel
{
}
тоді ми визначили б 3 кроки для майстра, де кожен крок, звичайно, міститиме лише ті властивості, які йому потрібні, а також відповідні атрибути перевірки:
[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; }
}
далі ми визначимо модель основної форми перегляду майстра, яка складається зі списку кроків та поточного індексу кроків:
[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();
}
}
Потім переходимо до контролера:
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);
}
}
Кілька зауважень щодо цього контролера:
- Дія Index POST використовує
[Deserialize]
атрибути з бібліотеки Microsoft Futures, тому переконайтеся, що ви встановили MvcContrib
NuGet. Ось чому причина перегляду моделей повинна бути прикрашена [Serializable]
атрибутом
- Дія індексу POST бере аргумент як
IStepViewModel
інтерфейс, щоб для цього мати сенс, нам потрібна прив'язка спеціальної моделі.
Ось пов’язана палітурка моделі:
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;
}
}
Це палітурне сполучення використовує спеціальне приховане поле під назвою StepType, яке буде містити конкретний тип кожного кроку і яке ми надсилатимемо для кожного запиту.
Це в'яжуча модель буде зареєстрована у Application_Start
:
ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());
Останній відсутній шматочок головоломки - це види. Ось головний ~/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" />
}
}
І це все, що потрібно для того, щоб це працювало. Звичайно, якщо ви хочете, ви можете персоналізувати зовнішній вигляд деяких або всіх кроків майстра, визначивши шаблон користувальницького редактора. Наприклад, зробимо це для кроку 2. Отже, ми визначимо ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml
часткове:
@model Step2ViewModel
Special Step 2
@Html.TextBoxFor(x => x.Bar)
Ось як виглядає структура:
Звичайно, є можливість для вдосконалення. Дія індексу POST виглядає як s..t. У ньому занадто багато коду. Подальше спрощення передбачає переміщення всіх елементів інфраструктури, таких як індекс, поточне управління індексом, копіювання поточного кроку в майстра, ... в інший палітур моделі. Так що нарешті ми закінчуємо:
[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);
}
більше того, як повинні виглядати дії POST. Я залишаю це покращення в наступний раз :-)