problemas de processo de registro de várias etapas no asp.net mvc (modelos de visualização divididos, modelo único)

117

Tenho um processo de registro de várias etapas , apoiado por um único objeto na camada de domínio , que tem regras de validação definidas nas propriedades.

Como devo validar o objeto de domínio quando o domínio é dividido em muitas visualizações e preciso salvar o objeto parcialmente na primeira visualização quando postado?

Pensei em usar Sessões, mas isso não é possível porque o processo é demorado e a quantidade de dados é alta, então não quero usar a sessão.

Pensei em salvar todos os dados em um banco de dados relacional na memória (com o mesmo esquema do banco de dados principal) e, em seguida, liberar esses dados para o banco de dados principal, mas surgiram problemas porque devo rotear entre os serviços (solicitados nas visualizações) que trabalham com o banco de dados principal e banco de dados na memória.

Estou procurando uma solução elegante e limpa (mais precisamente uma melhor prática).

ATUALIZAÇÃO E Esclarecimento:

@Darin Obrigado por sua resposta atenciosa, Isso foi exatamente o que fiz até agora. Mas, aliás, eu tenho uma solicitação que tem muitos anexos, eu desenho um, Step2Viewpor exemplo, qual usuário pode fazer upload de documentos de forma assíncrona, mas esses anexos devem ser salvos em uma tabela com relação referencial a outra tabela que deveria ter sido salva antes em Step1View.

Portanto, devo salvar o objeto de domínio em Step1(parcialmente), mas não posso, porque o objeto de Domínio Núcleo apoiado que é mapeado parcialmente para um ViewModel de Step1 não pode ser salvo sem adereços que vêm de convertidos Step2ViewModel.

Jahan
fonte
@Jani, você já descobriu como é o upload disso? Eu gostaria de escolher seu cérebro. Estou trabalhando exatamente neste problema.
Doug Chamberlain
1
A solução neste blog é bastante simples e direta. Ele usa divs como "etapas", alterando sua visibilidade e validação jquery discreta.
Dmitry Efimenko

Respostas:

229

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 WizardViewModelpara a visualização. Quando você estiver na primeira etapa dentro da ação do controlador, poderá inicializar a Step1propriedade. 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 WizardViewModelpreenchido 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 MvcContribNuGet. 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 IStepViewModelinterface, 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.cshtmlvisã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.cshtmlparcial:

@model Step2ViewModel

Special Step 2
@Html.TextBoxFor(x => x.Bar)

Veja como a estrutura se parece:

insira a descrição da imagem aqui

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 :-)

Darin Dimitrov
fonte
1
@Doug Chamberlain, eu uso o AutoMapper para converter entre meus modelos de visualização e modelos de domínio.
Darin Dimitrov de
1
@Doug Chamberlain, por favor, veja minha resposta atualizada. Espero que torne as coisas um pouco mais claras do que minha postagem inicial.
Darin Dimitrov
20
+1 @Jani: você realmente precisa dar a Darin os 50 pontos para esta resposta. É muito abrangente. E ele conseguiu reiterar a necessidade de usar ViewModel e não modelos de domínio ;-)
Tom Chantler
3
Não consigo encontrar o atributo Deserializar em lugar nenhum ... Também na página codeplex do mvccontrib, encontro este 94fa6078a115 por Jeremy Skinner 1 de agosto de 2010 às 17:55 0 Remova o fichário Deserializar obsoleto O que você me sugere fazer?
Chuck Norris
2
Encontrei um problema ao passo que não dei o nome de Etapa 1, Etapa 2, etc ... As minhas foram nomeadas com algo mais significativo, mas não em ordem alfabética. Então, acabei colocando meus modelos na ordem errada. Eu adicionei uma propriedade StepNumber à interface IStepViewModel. Agora posso classificar por isso no método Initialize de WizardViewModel.
Jeff Reddy
13

Para complementar a resposta de Amit Bagga, você encontrará abaixo o que fiz. Mesmo que menos elegante, acho esse caminho mais simples do que a resposta de Darin.

Controlador:

public ActionResult Step1()
{
    if (Session["wizard"] != null)
    {
        WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
        return View(wiz.Step1);
    }
    return View();
}

[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
    if (ModelState.IsValid)
    {
        WizardProductViewModel wiz = new WizardProductViewModel();
        wiz.Step1 = step1;
        //Store the wizard in session
        Session["wizard"] = wiz;
        return RedirectToAction("Step2");
    }
    return View(step1);
}

public ActionResult Step2()
{
    if (Session["wizard"] != null)
    {
        WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
        return View(wiz.Step2);
    }
    return View();
}

[HttpPost]
public ActionResult Step2(Step2ViewModel step2)
{
    if (ModelState.IsValid)
    {
        //Pull the wizard from session
        WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
        wiz.Step2 = step2;
        //Store the wizard in session
        Session["wizard"] = wiz;
        //return View("Step3");
        return RedirectToAction("Step3");
    }
    return View(step2);
}

public ActionResult Step3()
{
    WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
    return View(wiz.Step3);
}

[HttpPost]
public ActionResult Step3(Step3ViewModel step3)
{
    if (ModelState.IsValid)
    {
        //Pull the wizard from session
        WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
        wiz.Step3 = step3;
        //Save the data
        Product product = new Product
        {
            //Binding with view models
            Name = wiz.Step1.Name,
            ListPrice = wiz.Step2.ListPrice,
            DiscontinuedDate = wiz.Step3.DiscontinuedDate
        };

        db.Products.Add(product);
        db.SaveChanges();
        return RedirectToAction("Index", "Product");
    }
    return View(step3);
}

Modelos:

 [Serializable]
    public class Step1ViewModel 
    {
        [Required]
        [MaxLength(20, ErrorMessage="Longueur max de 20 caractères")]
        public string Name { get; set; }

    }

    [Serializable]
    public class Step2ViewModel
    {
        public Decimal ListPrice { get; set; }

    }

    [Serializable]
    public class Step3ViewModel
    {
        public DateTime? DiscontinuedDate { get; set; }
    }

    [Serializable]
    public class WizardProductViewModel
    {
        public Step1ViewModel Step1  { get; set; }
        public Step2ViewModel Step2  { get; set; }
        public Step3ViewModel Step3  { get; set; }
    }
Arno 2501
fonte
11

Eu sugeriria que você mantenha o estado de Processo Completo no cliente usando Jquery.

Por exemplo, temos um processo de assistente de três etapas.

  1. O usuário é apresentado com a Etapa 1 na qual tem um botão rotulado como "Próximo"
  2. Ao clicar em Avançar, fazemos uma solicitação Ajax e criamos um DIV chamado Step2 e carregamos o HTML nesse DIV.
  3. Na Etapa 3, temos um botão rotulado "Concluído" ao clicar no botão postar os dados usando $ .post call.

Desta forma, você pode construir facilmente seu objeto de domínio diretamente a partir dos dados de postagem do formulário e, caso os dados contenham erros, retorne um JSON válido contendo todas as mensagens de erro e exibi-los em um div.

Divida as etapas

public class Wizard 
{
  public Step1 Step1 {get;set;}
  public Step2 Step2 {get;set;}
  public Step3 Step3 {get;set;}
}

public ActionResult Step1(Step1 step)
{
  if(Model.IsValid)
 {
   Wizard wiz = new Wizard();
   wiz.Step1 = step;
  //Store the Wizard in Session;
  //Return the action
 }
}

public ActionResult Step2(Step2 step)
{
 if(Model.IsValid)
 {
   //Pull the Wizard From Session
   wiz.Step2=step;
 }
}

O Acima é apenas uma demonstração que o ajudará a alcançar o resultado final. Na etapa final, você deve criar o objeto de domínio e preencher os valores corretos do objeto do assistente e armazenar no banco de dados.

Amit Bagga
fonte
Sim, é uma solução interessante, mas infelizmente temos uma conexão de Internet ruim do lado do cliente, e ele / ela deve nos enviar um monte de arquivos. portanto, rejeitamos essa solução anteriormente.
Jahan
Você pode, por favor, me informar o volume de dados que o cliente enviará.
Amit Bagga
Vários arquivos, quase dez, cada um com cerca de 1 MB.
Jahan
5

Assistentes são apenas etapas simples no processamento de um modelo simples. Não há razão para criar vários modelos para um assistente. Tudo que você faria é criar um único modelo e passá-lo entre as ações em um único controlador.

public class MyModel
{
     [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
     public Guid Id { get; set };
     public string StepOneData { get; set; }
     public string StepTwoData { get; set; }
}

O coed acima é estúpido e simples, então substitua seus campos lá. Em seguida, começamos com uma ação simples que inicia nosso assistente.

    public ActionResult WizardStep1()
    {
        return View(new MyModel());
    }

Isso chama a visualização de "WizardStep1.cshtml (se estiver usando razor). Você pode usar o assistente de criação de modelo se quiser. Estaremos apenas redirecionando a postagem para uma ação diferente.

<WizardStep1.cshtml>
@using (Html.BeginForm("WizardStep2", "MyWizard")) {

O importante é que postaremos isso em uma ação diferente; a ação WizardStep2

    [HttpPost]
    public ActionResult WizardStep2(MyModel myModel)
    {
        return ModelState.IsValid ? View(myModel) : View("WizardStep1", myModel);
    }

Nesta ação, verificamos se nosso modelo é válido e, em caso afirmativo, o enviamos para nossa view WizardStep2.cshtml, caso contrário, o enviamos de volta à etapa um com os erros de validação. Em cada etapa, enviamos para a próxima etapa, validar essa etapa e seguir em frente. Agora, alguns desenvolvedores experientes podem dizer que não podemos nos mover entre etapas como esta se usarmos atributos [Obrigatórios] ou outras anotações de dados entre as etapas. E você estaria certo, então remova os erros nos itens que ainda não foram verificados. como abaixo.

    [HttpPost]
    public ActionResult WizardStep3(MyModel myModel)
    {
        foreach (var error in ModelState["StepTwoData"].Errors)
        {
            ModelState["StepTwoData"].Errors.Remove(error);
        }

Por fim, salvaríamos o modelo uma vez no armazenamento de dados. Isso também evita que um usuário inicie um assistente, mas não o conclua, de não salvar dados incompletos no banco de dados.

Espero que você ache este método de implementação de um assistente muito mais fácil de usar e manter do que qualquer um dos métodos mencionados anteriormente.

Obrigado pela leitura.

Darroll
fonte
você tem uma solução completa que eu possa experimentar? Obrigado
mpora
5

Eu queria compartilhar minha própria maneira de lidar com esses requisitos. Eu não queria usar SessionState, nem queria lidar com o lado do cliente, e o método serialize requer MVC Futures que eu não queria incluir em meu projeto.

Em vez disso, construí um HTML Helper que irá iterar por todas as propriedades do modelo e gerar um elemento oculto personalizado para cada uma. Se for uma propriedade complexa, ela será executada recursivamente nela.

Em seu formulário, eles serão postados no controlador junto com os novos dados do modelo a cada etapa do "assistente".

Eu escrevi isso para MVC 5.

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Web;
using System.Web.Routing;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Reflection;

namespace YourNamespace
{
    public static class CHTML
    {
        public static MvcHtmlString HiddenClassFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
        {
            return HiddenClassFor(html, expression, null);
        }

        public static MvcHtmlString HiddenClassFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
        {
            ModelMetadata _metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);

            if (_metaData.Model == null)
                return MvcHtmlString.Empty;

            RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;

            return MvcHtmlString.Create(HiddenClassFor(html, expression, _metaData, _dict).ToString());
        }

        private static StringBuilder HiddenClassFor<TModel>(HtmlHelper<TModel> html, LambdaExpression expression, ModelMetadata metaData, IDictionary<string, object> htmlAttributes)
        {
            StringBuilder _sb = new StringBuilder();

            foreach (ModelMetadata _prop in metaData.Properties)
            {
                Type _type = typeof(Func<,>).MakeGenericType(typeof(TModel), _prop.ModelType);
                var _body = Expression.Property(expression.Body, _prop.PropertyName);
                LambdaExpression _propExp = Expression.Lambda(_type, _body, expression.Parameters);

                if (!_prop.IsComplexType)
                {
                    string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(_propExp));
                    string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(_propExp));
                    object _value = _prop.Model;

                    _sb.Append(MinHiddenFor(_id, _name, _value, htmlAttributes));
                }
                else
                {
                    if (_prop.ModelType.IsArray)
                        _sb.Append(HiddenArrayFor(html, _propExp, _prop, htmlAttributes));
                    else if (_prop.ModelType.IsClass)
                        _sb.Append(HiddenClassFor(html, _propExp, _prop, htmlAttributes));
                    else
                        throw new Exception(string.Format("Cannot handle complex property, {0}, of type, {1}.", _prop.PropertyName, _prop.ModelType));
                }
            }

            return _sb;
        }

        public static MvcHtmlString HiddenArrayFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
        {
            return HiddenArrayFor(html, expression, null);
        }

        public static MvcHtmlString HiddenArrayFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
        {
            ModelMetadata _metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);

            if (_metaData.Model == null)
                return MvcHtmlString.Empty;

            RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;

            return MvcHtmlString.Create(HiddenArrayFor(html, expression, _metaData, _dict).ToString());
        }

        private static StringBuilder HiddenArrayFor<TModel>(HtmlHelper<TModel> html, LambdaExpression expression, ModelMetadata metaData, IDictionary<string, object> htmlAttributes)
        {
            Type _eleType = metaData.ModelType.GetElementType();
            Type _type = typeof(Func<,>).MakeGenericType(typeof(TModel), _eleType);

            object[] _array = (object[])metaData.Model;

            StringBuilder _sb = new StringBuilder();

            for (int i = 0; i < _array.Length; i++)
            {
                var _body = Expression.ArrayIndex(expression.Body, Expression.Constant(i));
                LambdaExpression _arrayExp = Expression.Lambda(_type, _body, expression.Parameters);
                ModelMetadata _valueMeta = ModelMetadata.FromLambdaExpression((dynamic)_arrayExp, html.ViewData);

                if (_eleType.IsClass)
                {
                    _sb.Append(HiddenClassFor(html, _arrayExp, _valueMeta, htmlAttributes));
                }
                else
                {
                    string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(_arrayExp));
                    string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(_arrayExp));
                    object _value = _valueMeta.Model;

                    _sb.Append(MinHiddenFor(_id, _name, _value, htmlAttributes));
                }
            }

            return _sb;
        }

        public static MvcHtmlString MinHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
        {
            return MinHiddenFor(html, expression, null);
        }

        public static MvcHtmlString MinHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
        {
            string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(expression));
            string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression));
            object _value = ModelMetadata.FromLambdaExpression(expression, html.ViewData).Model;
            RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;

            return MinHiddenFor(_id, _name, _value, _dict);
        }

        public static MvcHtmlString MinHiddenFor(string id, string name, object value, IDictionary<string, object> htmlAttributes)
        {
            TagBuilder _input = new TagBuilder("input");
            _input.Attributes.Add("id", id);
            _input.Attributes.Add("name", name);
            _input.Attributes.Add("type", "hidden");

            if (value != null)
            {
                _input.Attributes.Add("value", value.ToString());
            }

            if (htmlAttributes != null)
            {
                foreach (KeyValuePair<string, object> _pair in htmlAttributes)
                {
                    _input.MergeAttribute(_pair.Key, _pair.Value.ToString(), true);
                }
            }

            return new MvcHtmlString(_input.ToString(TagRenderMode.SelfClosing));
        }
    }
}

Agora, para todas as etapas do seu "assistente", você pode usar o mesmo modelo básico e passar as propriedades do modelo "Etapa 1,2,3" para o auxiliar @ Html.HiddenClassFor usando uma expressão lambda.

Você pode até ter um botão Voltar em cada etapa, se desejar. Apenas tenha um botão voltar em seu formulário que irá postá-lo em uma ação StepNBack no controlador usando o atributo formaction. Não incluído no exemplo abaixo, mas apenas uma ideia para você.

De qualquer forma, aqui está um exemplo básico:

Aqui está o seu MODELO

public class WizardModel
{
    // you can store additional properties for your "wizard" / parent model here
    // these properties can be saved between pages by storing them in the form using @Html.MinHiddenFor(m => m.WizardID)
    public int? WizardID { get; set; }

    public string WizardType { get; set; }

    [Required]
    public Step1 Step1 { get; set; }

    [Required]
    public Step2 Step2 { get; set; }

    [Required]
    public Step3 Step3 { get; set; }

    // if you want to use the same model / view / controller for EDITING existing data as well as submitting NEW data here is an example of how to handle it
    public bool IsNew
    {
        get
        {
            return WizardID.HasValue;
        }
    }
}

public class Step1
{
    [Required]
    [MaxLength(32)]
    [Display(Name = "First Name")]
    public string FirstName { get; set; }

    [Required]
    [MaxLength(32)]
    [Display(Name = "Last Name")]
    public string LastName { get; set; }
}

public class Step2
{
    [Required]
    [MaxLength(512)]
    [Display(Name = "Biography")]
    public string Biography { get; set; }
}

public class Step3
{        
    // lets have an array of strings here to shake things up
    [Required]
    [Display(Name = "Your Favorite Foods")]
    public string[] FavoriteFoods { get; set; }
}

Aqui está o seu CONTROLADOR

public class WizardController : Controller
{
    [HttpGet]
    [Route("wizard/new")]
    public ActionResult New()
    {
        WizardModel _model = new WizardModel()
        {
            WizardID = null,
            WizardType = "UserInfo"
        };

        return View("Step1", _model);
    }

    [HttpGet]
    [Route("wizard/edit/{wizardID:int}")]
    public ActionResult Edit(int wizardID)
    {
        WizardModel _model = database.GetData(wizardID);

        return View("Step1", _model);
    }

    [HttpPost]
    [Route("wizard/step1")]
    public ActionResult Step1(WizardModel model)
    {
        // just check if the values in the step1 model are valid
        // shouldn't use ModelState.IsValid here because that would check step2 & step3.
        // which isn't entered yet
        if (ModelState.IsValidField("Step1"))
        {
            return View("Step2", model);
        }

        return View("Step1", model);
    }

    [HttpPost]
    [Route("wizard/step2")]
    public ActionResult Step2(WizardModel model)
    {
        if (ModelState.IsValidField("Step2"))
        {
            return View("Step3", model);
        }

        return View("Step2", model);
    }

    [HttpPost]
    [Route("wizard/step3")]
    public ActionResult Step3(WizardModel model)
    {
        // all of the data for the wizard model is complete.
        // so now we check the entire model state
        if (ModelState.IsValid)
        {
            // validation succeeded. save the data from the model.
            // the model.IsNew is just if you want users to be able to
            // edit their existing data.
            if (model.IsNew)
                database.NewData(model);
            else
                database.EditData(model);

            return RedirectToAction("Success");
        }

        return View("Step3", model);
    }
}

Aqui estão suas VIEWS

Passo 1

@model WizardModel

@{
    ViewBag.Title = "Step 1";
}

@using (Html.BeginForm("Step1", "Wizard", FormMethod.Post))
{
    @Html.MinHiddenFor(m => m.WizardID)
    @Html.MinHiddenFor(m => m.WizardType)

    @Html.LabelFor(m => m.Step1.FirstName)
    @Html.TextBoxFor(m => m.Step1.FirstName)

    @Html.LabelFor(m => m.Step1.LastName)
    @Html.TextBoxFor(m => m.Step1.LastName)

    <button type="submit">Submit</button>
}

Passo 2

@model WizardModel

@{
    ViewBag.Title = "Step 2";
}

@using (Html.BeginForm("Step2", "Wizard", FormMethod.Post))
{
    @Html.MinHiddenFor(m => m.WizardID)
    @Html.MinHiddenFor(m => m.WizardType)
    @Html.HiddenClassFor(m => m.Step1)

    @Html.LabelFor(m => m.Step2.Biography)
    @Html.TextAreaFor(m => m.Step2.Biography)

    <button type="submit">Submit</button>
}

etapa 3

@model WizardModel

@{
    ViewBag.Title = "Step 3";
}

@using (Html.BeginForm("Step3", "Wizard", FormMethod.Post))
{
    @Html.MinHiddenFor(m => m.WizardID)
    @Html.MinHiddenFor(m => m.WizardType)
    @Html.HiddenClassFor(m => m.Step1)
    @Html.HiddenClassFor(m => m.Step2)

    @Html.LabelFor(m => m.Step3.FavoriteFoods)
    @Html.ListBoxFor(m => m.Step3.FavoriteFoods,
        new SelectListItem[]
        {
            new SelectListItem() { Value = "Pizza", Text = "Pizza" },
            new SelectListItem() { Value = "Sandwiches", Text = "Sandwiches" },
            new SelectListItem() { Value = "Burgers", Text = "Burgers" },
        });

    <button type="submit">Submit</button>
}
ArcadeRenegade
fonte
1
Você poderia esclarecer ainda mais sua solução fornecendo o modelo de visualização e o controlador?
Tyler Durden
2

Adicionando mais informações da resposta de @Darin.

E se você tiver um estilo de design separado para cada etapa e desejar manter cada uma em uma visualização parcial separada ou se você tiver várias propriedades para cada etapa?

Durante o uso Html.EditorFor, temos limitações para usar a visualização parcial.

Crie 3 vistas parciais na Sharedpasta chamada:Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

Para resumir, acabei de postar a primeira visão parcial, as outras etapas são as mesmas da resposta de Darin.

Step1ViewModel.cs

[Serializable]
public class Step1ViewModel : IStepViewModel
{
  [Required]
  public string FirstName { get; set; }

  public string LastName { get; set; }

  public string PhoneNo { get; set; }

  public string EmailId { get; set; }

  public int Age { get; set; }

 }

Step1ViewModel.cshtml

 @model WizardPages.ViewModels.Step1ViewModel

<div class="container">
    <h2>Personal Details</h2>

    <div class="form-group">
        <label class="control-label col-sm-2" for="email">First Name:</label>
        <div class="col-sm-10">
            @Html.TextBoxFor(x => x.FirstName)
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-2" for="pwd">Last Name:</label>
        <div class="col-sm-10">
            @Html.TextBoxFor(x => x.LastName)
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-2" for="pwd">Phone No:</label>
        <div class="col-sm-10"> 
            @Html.TextBoxFor(x => x.PhoneNo)
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-2" for="pwd">Email Id:</label>
        <div class="col-sm-10">
            @Html.TextBoxFor(x => x.EmailId)
        </div>
    </div>


</div>

Index.cshtml

@using Microsoft.Web.Mvc
@model WizardPages.ViewModels.WizardViewModel

@{
    var currentStep = Model.Steps[Model.CurrentStepIndex];

    string viewName = currentStep.ToString().Substring(currentStep.ToString().LastIndexOf('.') + 1);
}

<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.Partial(""+ viewName + "", currentStep);

    if (Model.CurrentStepIndex > 0)
    {

     <input type="submit" value="Previous" name="prev" class="btn btn-warning" />

    }

    if (Model.CurrentStepIndex < Model.Steps.Count - 1)
    {

      <input type="submit" value="Next" name="next" class="btn btn-info" />

    }
    else
    {

      <input type="submit" value="Finish" name="finish" class="btn btn-success" />

    }
}

Se houver alguma solução melhor, por favor, comente para que outros saibam.

shaijut
fonte
-9

Uma opção é criar um conjunto de tabelas idênticas que armazenarão os dados coletados em cada etapa. Então, na última etapa, se tudo correr bem, você pode criar a entidade real copiando os dados temporários e armazenando-os.

Outra é criar Value Objectspara cada etapa e armazená-la em Cacheou Session. Então, se tudo correr bem, você pode criar seu objeto Domínio a partir deles e salvá-lo

Amila silva
fonte
1
Seria bom se as pessoas que votam contra também apresentassem seus motivos.
Martin
Não votou contra você, mas sua resposta é completamente irrelevante para a pergunta. O OP está perguntando sobre como criar o assistente, enquanto você responde sobre como lidar com a resposta no verso.
Demência de
1
Eu normalmente não voto, mas quando o faço, certifico-me de que seja a favor :-)
Suhail Mumtaz Awan