Во-первых, вы не должны использовать в своих представлениях какие-либо объекты домена. Вы должны использовать модели просмотра. Каждая модель представления будет содержать только те свойства, которые требуются для данного представления, а также атрибуты проверки, специфичные для данного представления. Таким образом, если у вас есть мастер из 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 - хороший вариант ). В основном это вопрос отображения и скрытия div на клиенте, и в этом случае вам больше не нужно беспокоиться о сохранении состояния между этапами.
Но независимо от того, какое решение вы выберете, всегда используйте модели представления и выполняйте проверку этих моделей представления. Пока вы прикрепляете атрибуты проверки аннотации данных к своим моделям предметной области, вам будет очень трудно, поскольку модели предметной области не адаптированы к представлениям.
ОБНОВИТЬ:
Хорошо, по многочисленным комментариям делаю вывод, что мой ответ был непонятен. И я должен согласиться. Итак, позвольте мне попытаться продолжить мой пример.
Мы могли бы определить интерфейс, который должны реализовывать все модели пошаговых представлений (это просто интерфейс маркера):
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]
атрибутом
- Действие Index 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)
Вот как выглядит конструкция:
Конечно, есть возможности для улучшения. Действие Index 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. Оставляю это улучшение в следующий раз :-)