Primeiro, você não deve usar nenhum objeto de domínio em suas visualizações. Você deve usar modelos de visualização. Cada modelo de visualização conterá apenas as propriedades que são exigidas por determinada visualização, bem como os atributos de validação específicos para esta determinada visualização. Portanto, se você tiver um assistente de 3 etapas, isso significa que você terá 3 modelos de visualização, um para cada etapa:
public class Step1ViewModel
{
[Required]
public string SomeProperty { get; set; }
...
}
public class Step2ViewModel
{
[Required]
public string SomeOtherProperty { get; set; }
...
}
e assim por diante. Todos esses modelos de visualização podem ser apoiados por um modelo de visualização do assistente principal:
public class WizardViewModel
{
public Step1ViewModel Step1 { get; set; }
public Step2ViewModel Step2 { get; set; }
...
}
então você poderia ter ações do controlador renderizando cada etapa do processo do assistente e passando o principal WizardViewModel
para a visualização. Quando você estiver na primeira etapa dentro da ação do controlador, poderá inicializar a Step1
propriedade. Então, dentro da visão, você geraria o formulário permitindo ao usuário preencher as propriedades sobre a etapa 1. Quando o formulário for enviado, a ação do controlador aplicará as regras de validação apenas para a etapa 1:
[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
var model = new WizardViewModel
{
Step1 = step1
};
if (!ModelState.IsValid)
{
return View(model);
}
return View("Step2", model);
}
Agora, na visualização da etapa 2, você pode usar o auxiliar Html.Serialize de MVC futuros para serializar a etapa 1 em um campo oculto dentro do formulário (uma espécie de ViewState, se desejar):
@using (Html.BeginForm("Step2", "Wizard"))
{
@Html.Serialize("Step1", Model.Step1)
@Html.EditorFor(x => x.Step2)
...
}
e dentro da ação POST da etapa 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);
}
E assim por diante até chegar à última etapa em que terá o WizardViewModel
preenchido com todos os dados. Em seguida, você mapeará o modelo de visualização para seu modelo de domínio e o passará para a camada de serviço para processamento. A camada de serviço pode executar qualquer regra de validação e assim por diante ...
Existe também outra alternativa: usar javascript e colocar tudo na mesma página. Existem muitos plug - ins jquery por aí que fornecem funcionalidade de assistente ( Stepy é um bom exemplo). É basicamente uma questão de mostrar e ocultar divs no cliente, caso em que você não precisa mais se preocupar com a persistência do estado entre as etapas.
Mas não importa a solução que você escolher, sempre use modelos de visualização e execute a validação nesses modelos de visualização. Enquanto você estiver utilizando atributos de validação de anotação de dados em seus modelos de domínio, terá muita dificuldade, pois os modelos de domínio não são adaptados para visualizações.
ATUALIZAR:
OK, devido aos inúmeros comentários, concluo que a minha resposta não foi clara. E devo concordar. Portanto, deixe-me tentar elaborar mais detalhadamente meu exemplo.
Poderíamos definir uma interface que todos os modelos de visualização de etapas devem implementar (é apenas uma interface de marcador):
public interface IStepViewModel
{
}
então, definiríamos 3 etapas para o assistente, em que cada etapa, é claro, conteria apenas as propriedades necessárias, bem como os atributos de validação relevantes:
[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; }
}
a seguir, definimos o modelo de visualização do assistente principal, que consiste em uma lista de etapas e um índice de etapa atual:
[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();
}
}
Em seguida, passamos para o controlador:
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);
}
}
Algumas observações sobre este controlador:
- A ação Index POST usa os
[Deserialize]
atributos da biblioteca Microsoft Futures, portanto, certifique-se de ter instalado o MvcContrib
NuGet. Essa é a razão pela qual os modelos de visualização devem ser decorados com o [Serializable]
atributo
- A ação Index POST leva como argumento uma
IStepViewModel
interface, portanto, para fazer sentido, precisamos de um fichário de modelo personalizado.
Aqui está o fichário de modelo associado:
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;
}
}
Esse fichário usa um campo oculto especial denominado StepType que conterá o tipo concreto de cada etapa e que enviaremos em cada solicitação.
Este modelo de fichário será registrado em Application_Start
:
ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());
A última parte que falta no quebra-cabeça são as visualizações. Esta é a ~/Views/Wizard/Index.cshtml
visão principal :
@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" />
}
}
E isso é tudo de que você precisa para fazer isso funcionar. Claro, se você quiser, pode personalizar a aparência de algumas ou todas as etapas do assistente definindo um modelo de editor personalizado. Por exemplo, vamos fazer isso para a etapa 2. Portanto, definimos um ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml
parcial:
@model Step2ViewModel
Special Step 2
@Html.TextBoxFor(x => x.Bar)
Veja como a estrutura se parece:
Claro que há espaço para melhorias. A ação Index POST se parece com s..t. Há muito código nele. Uma simplificação adicional envolveria mover todas as coisas de infraestrutura como índice, gerenciamento de índice atual, cópia da etapa atual no assistente, ... para outro fichário de modelo. Então, finalmente, terminamos com:
[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);
}
que é mais como as ações POST devem se parecer. Estou deixando essa melhoria para a próxima vez :-)