Во-первых, вы не должны использовать какие-либо доменные объекты в своих представлениях.Вы должны использовать просмотр моделей.Каждая модель представления будет содержать только те свойства, которые требуются для данного представления, а также атрибуты проверки, специфичные для данного представления.Таким образом, если у вас есть мастер 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 helper из фьючерсов 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);
}
}
Пара замечаний по поводу этого контроллера:
- Индекс 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
partical:
@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. Я оставляю это улучшение в следующий раз: -)