problèmes de processus d'enregistrement en plusieurs étapes dans asp.net mvc (modèles de vue fractionnée, modèle unique)

117

J'ai un processus d'enregistrement en plusieurs étapes , soutenu par un seul objet dans la couche de domaine , qui a des règles de validation définies sur les propriétés.

Comment valider l'objet de domaine lorsque le domaine est divisé en plusieurs vues et que je dois enregistrer l'objet partiellement dans la première vue lors de sa publication?

J'ai pensé à utiliser Sessions mais ce n'est pas possible car le processus est long et la quantité de données est élevée, donc je ne veux pas utiliser session.

J'ai pensé à enregistrer toutes les données dans une base de données relationnelle en mémoire (avec le même schéma que la base de données principale), puis à vider ces données dans la base de données principale, mais des problèmes sont survenus car je devrais acheminer entre les services (demandés dans les vues) qui travaillent avec le base de données principale et base de données en mémoire.

Je recherche une solution élégante et propre (plus précisément une meilleure pratique).

MISE À JOUR ET Clarification:

@Darin Merci pour votre réponse réfléchie, c'est exactement ce que j'ai fait jusqu'à présent. Mais accessoirement, j'ai une demande qui contient de nombreuses pièces jointes, je conçois un Step2Viewexemple, quel utilisateur peut télécharger des documents de manière asynchrone, mais ces pièces jointes doivent être enregistrées dans une table avec une relation référentielle avec une autre table qui aurait dû être enregistrée auparavant dans Step1View.

Ainsi, je devrais enregistrer l'objet de domaine dans Step1(partiellement), mais je ne peux pas, car l'objet de domaine principal sauvegardé qui est mappé partiellement à ViewModel de Step1 ne peut pas être enregistré sans les accessoires qui proviennent de convertis Step2ViewModel.

Jahan
la source
@Jani, avez-vous déjà trouvé la partie de ce téléchargement? J'aimerais choisir votre cerveau. Je travaille sur ce problème précis.
Doug Chamberlain
1
La solution de ce blog est assez simple et directe. Il utilise les divs comme "étapes" en inversant leur visibilité et leur validation discrète de jquery.
Dmitry Efimenko

Réponses:

229

Tout d'abord, vous ne devez utiliser aucun objet de domaine dans vos vues. Vous devriez utiliser des modèles de vue. Chaque modèle de vue contiendra uniquement les propriétés requises par la vue donnée ainsi que les attributs de validation spécifiques à cette vue donnée. Donc, si vous avez un assistant en 3 étapes, cela signifie que vous aurez 3 modèles de vue, un pour chaque étape:

public class Step1ViewModel
{
    [Required]
    public string SomeProperty { get; set; }

    ...
}

public class Step2ViewModel
{
    [Required]
    public string SomeOtherProperty { get; set; }

    ...
}

etc. Tous ces modèles de vue peuvent être soutenus par un modèle de vue principal de l'assistant:

public class WizardViewModel
{
    public Step1ViewModel Step1 { get; set; }
    public Step2ViewModel Step2 { get; set; }
    ...
}

alors vous pourriez avoir des actions de contrôleur rendant chaque étape du processus de l'assistant et passant le principal WizardViewModelà la vue. Lorsque vous êtes à la première étape de l'action du contrôleur, vous pouvez initialiser la Step1propriété. Ensuite, dans la vue, vous générez le formulaire permettant à l'utilisateur de remplir les propriétés de l'étape 1. Lorsque le formulaire est soumis, l'action du contrôleur appliquera les règles de validation pour l'étape 1 uniquement:

[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
    var model = new WizardViewModel 
    {
        Step1 = step1
    };

    if (!ModelState.IsValid)
    {
        return View(model);
    }
    return View("Step2", model);
}

Maintenant, dans la vue de l'étape 2, vous pouvez utiliser l' assistant Html.Serialize des futurs MVC afin de sérialiser l'étape 1 dans un champ caché à l'intérieur du formulaire (sorte de ViewState si vous le souhaitez):

@using (Html.BeginForm("Step2", "Wizard"))
{
    @Html.Serialize("Step1", Model.Step1)
    @Html.EditorFor(x => x.Step2)
    ...
}

et à l'intérieur de l'action POST de l'étape 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);
}

Et ainsi de suite jusqu'à ce que vous arriviez à la dernière étape où vous aurez WizardViewModelrempli toutes les données. Ensuite, vous allez mapper le modèle de vue sur votre modèle de domaine et le transmettre à la couche de service pour traitement. La couche de service peut exécuter toutes les règles de validation elle-même et ainsi de suite ...

Il existe également une autre alternative: utiliser javascript et mettre tout sur la même page. Il existe de nombreux plugins jquery qui fournissent des fonctionnalités d'assistant ( Stepy est un bon). Il s'agit essentiellement d'afficher et de masquer les divs sur le client, auquel cas vous n'avez plus à vous soucier de l'état persistant entre les étapes.

Mais quelle que soit la solution que vous choisissez, utilisez toujours des modèles de vue et effectuez la validation sur ces modèles de vue. Tant que vous collerez des attributs de validation d'annotation de données sur vos modèles de domaine, vous aurez beaucoup de mal car les modèles de domaine ne sont pas adaptés aux vues.


METTRE À JOUR:

OK, en raison des nombreux commentaires, je tire la conclusion que ma réponse n'était pas claire. Et je dois être d'accord. Alors laissez-moi essayer de développer davantage mon exemple.

Nous pourrions définir une interface que tous les modèles de vue pas à pas devraient implémenter (c'est juste une interface de marqueur):

public interface IStepViewModel
{
}

puis nous définirions 3 étapes pour l'assistant où chaque étape ne contiendrait bien sûr que les propriétés dont elle a besoin ainsi que les attributs de validation pertinents:

[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; }
}

Ensuite, nous définissons le modèle de vue de l'assistant principal qui se compose d'une liste d'étapes et d'un index d'étape actuel:

[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();
    }
}

Ensuite, nous passons au contrôleur:

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);
    }
}

Quelques remarques sur ce contrôleur:

  • L'action Index POST utilise les [Deserialize]attributs de la bibliothèque Microsoft Futures afin de vous assurer que vous avez installé MvcContribNuGet. C'est la raison pour laquelle les modèles de vue doivent être décorés avec l' [Serializable]attribut
  • L'action Index POST prend comme argument une IStepViewModelinterface, donc pour que cela ait un sens, nous avons besoin d'un classeur de modèles personnalisé.

Voici le classeur de modèles associé:

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;
    }
}

Ce classeur utilise un champ caché spécial appelé StepType qui contiendra le type concret de chaque étape et que nous enverrons à chaque demande.

Ce modèle de classeur sera enregistré dans Application_Start:

ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());

Le dernier élément manquant du puzzle sont les vues. Voici la ~/Views/Wizard/Index.cshtmlvue principale :

@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" />
    }
}

Et c'est tout ce dont vous avez besoin pour que cela fonctionne. Bien sûr, si vous le souhaitez, vous pouvez personnaliser l'apparence de certaines ou de toutes les étapes de l'assistant en définissant un modèle d'éditeur personnalisé. Par exemple, faisons-le pour l'étape 2. Nous définissons donc un ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtmlpartiel:

@model Step2ViewModel

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

Voici à quoi ressemble la structure:

entrez la description de l'image ici

Bien entendu, il y a place à amélioration. L'action Index POST ressemble à s..t. Il y a trop de code dedans. Une simplification supplémentaire impliquerait de déplacer tous les éléments de l'infrastructure comme l'index, la gestion actuelle des index, la copie de l'étape actuelle dans l'assistant, ... dans un autre classeur de modèles. Alors que finalement nous nous retrouvons avec:

[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);
}

c'est plutôt à quoi devraient ressembler les actions POST. Je laisse cette amélioration pour la prochaine fois :-)

Darin Dimitrov
la source
1
@Doug Chamberlain, j'utilise AutoMapper pour convertir entre mes modèles de vue et mes modèles de domaine.
Darin Dimitrov
1
@Doug Chamberlain, s'il vous plaît voir ma réponse mise à jour. J'espère que cela rend les choses un peu plus claires que mon message initial.
Darin Dimitrov le
20
+1 @Jani: vous devez vraiment donner à Darin les 50 points pour cette réponse. C'est très complet. Et il a réussi à réitérer la nécessité d'utiliser ViewModel et non les modèles de domaine ;-)
Tom Chantler
3
Je ne trouve pas l'attribut Désérialiser nulle part ... Aussi dans la page codeplex de mvccontrib, je trouve ce 94fa6078a115 par Jeremy Skinner 1 août 2010 à 17 h 55 0 Supprimer le classeur désérialisé désérialisé Que me suggérez-vous de faire?
Chuck Norris
2
J'ai trouvé un problème alors que je n'ai pas nommé mes vues Step1, Step2, etc ... Les miennes sont nommées quelque chose de plus significatif, mais pas alphabétique. Donc, j'ai fini par mettre mes modèles dans le mauvais ordre. J'ai ajouté une propriété StepNumber à l'interface IStepViewModel. Maintenant, je peux trier par ceci dans la méthode Initialize de WizardViewModel.
Jeff Reddy
13

Pour compléter la réponse d'Amit Bagga, vous trouverez ci-dessous ce que j'ai fait. Même si c'est moins élégant, je trouve cette voie plus simple que la réponse de Darin.

Manette :

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);
}

Des modèles :

 [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
la source
11

Je vous suggère de maintenir l'état du processus complet sur le client à l'aide de Jquery.

Par exemple, nous avons un processus d'assistant en trois étapes.

  1. L'utilisateur a présenté le Step1 sur lequel a un bouton étiqueté "Suivant"
  2. En cliquant sur Suivant, nous faisons une requête Ajax et créons un DIV appelé Step2 et chargeons le HTML dans ce DIV.
  3. À l'étape 3, nous avons un bouton étiqueté «Terminé» en cliquant sur le bouton pour publier les données en utilisant l'appel $ .post.

De cette façon, vous pouvez facilement créer votre objet de domaine directement à partir des données de publication du formulaire et au cas où les données contiendraient des erreurs, retournez un JSON valide contenant tous les messages d'erreur et les afficher dans un div.

Veuillez diviser les étapes

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;
 }
}

Le ci-dessus est juste une démonstration qui vous aidera à atteindre le résultat final. À l'étape finale, vous devez créer l'objet de domaine et remplir les valeurs correctes de l'objet de l'assistant et du magasin dans la base de données.

Amit Bagga
la source
Oui, c'est une solution intéressante, mais nous avons malheureusement une mauvaise connexion Internet côté client, et il / elle devrait nous envoyer un tas de fichiers. nous avons donc rejeté cette solution plus tôt.
Jahan
Pouvez-vous s'il vous plaît me faire savoir le volume de données que le client va télécharger.
Amit Bagga
Plusieurs fichiers, presque dix, chacun près de 1 Mo.
Jahan
5

Les assistants ne sont que de simples étapes de traitement d'un modèle simple. Il n'y a aucune raison de créer plusieurs modèles pour un assistant. Tout ce que vous feriez est de créer un modèle unique et de le transmettre entre les actions dans un seul contrôleur.

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

L'étudiante ci-dessus est stupide, alors remplacez vos champs là-dedans. Ensuite, nous commençons par une action simple qui lance notre assistant.

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

Cela appelle la vue "WizardStep1.cshtml (si vous utilisez un rasoir). Vous pouvez utiliser l'assistant de création de modèle si vous le souhaitez. Nous redirigerons simplement le message vers une action différente.

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

La chose à noter est que nous publierons ceci dans une action différente; l'action WizardStep2

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

Dans cette action, nous vérifions si notre modèle est valide, et si c'est le cas, nous l'envoyons à notre vue WizardStep2.cshtml, sinon nous le renvoyons à la première étape avec les erreurs de validation. À chaque étape, nous l'envoyons à l'étape suivante, validons cette étape et passons à autre chose. Maintenant, certains développeurs avertis pourraient bien dire que nous ne pouvons pas passer d'une étape à l'autre si nous utilisons des attributs [Obligatoire] ou d'autres annotations de données entre les étapes. Et vous auriez raison, alors supprimez les erreurs sur les éléments qui doivent encore être vérifiés. comme ci-dessous.

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

Enfin, nous enregistrerions le modèle une fois dans le magasin de données. Cela empêche également un utilisateur qui démarre un assistant mais ne le termine pas de ne pas enregistrer des données incomplètes dans la base de données.

J'espère que vous trouverez cette méthode d'implémentation d'un assistant beaucoup plus facile à utiliser et à maintenir que n'importe laquelle des méthodes mentionnées précédemment.

Merci d'avoir lu.

Darroll
la source
avez-vous cela dans une solution complète que je peux essayer? Merci
mpora
5

Je voulais partager ma propre façon de gérer ces exigences. Je ne voulais pas du tout utiliser SessionState, je ne voulais pas non plus qu'il soit géré côté client, et la méthode de sérialisation nécessite MVC Futures que je ne voulais pas avoir à inclure dans mon projet.

Au lieu de cela, j'ai construit un HTML Helper qui parcourra toutes les propriétés du modèle et générera un élément caché personnalisé pour chacune. S'il s'agit d'une propriété complexe, elle s'exécutera de manière récursive dessus.

Dans votre formulaire, ils seront publiés sur le contrôleur avec les nouvelles données du modèle à chaque étape de "l'assistant".

J'ai écrit ceci pour 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));
        }
    }
}

Maintenant, pour toutes les étapes de votre "assistant", vous pouvez utiliser le même modèle de base et transmettre les propriétés du modèle "Étape 1, 2, 3" à l'aide @ Html.HiddenClassFor en utilisant une expression lambda.

Vous pouvez même avoir un bouton de retour à chaque étape si vous le souhaitez. Il suffit d'avoir un bouton de retour dans votre formulaire qui le publiera dans une action StepNBack sur le contrôleur en utilisant l'attribut formaction. Non inclus dans l'exemple ci-dessous mais juste une idée pour vous.

Quoi qu'il en soit, voici un exemple de base:

Voici votre modèle

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; }
}

Voici votre CONTRÔLEUR

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);
    }
}

Voici vos VUES

Étape 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>
}

Étape 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>
}

Étape 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>
}
ArcadeRénégade
la source
1
Pourriez-vous clarifier davantage votre solution en fournissant le modèle de vue et le contrôleur?
Tyler Durden
2

Ajout de plus d'informations à partir de la réponse de @ Darin.

Que se passe-t-il si vous avez un style de conception distinct pour chaque étape et que vous souhaitez conserver chacune dans une vue partielle distincte ou que faire si vous avez plusieurs propriétés pour chaque étape?

Lors de l'utilisation, Html.EditorFornous avons la limitation d'utiliser la vue partielle.

Créez 3 vues partielles sous le Shareddossier nommé:Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

Par souci de concision, je viens de poster la 1ère vue patiale, les autres étapes sont identiques à la réponse 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" />

    }
}

S'il existe une meilleure solution, veuillez commenter pour en informer les autres.

shaijut
la source
-9

Une option consiste à créer un ensemble de tables identiques qui stockeront les données collectées à chaque étape. Ensuite, dans la dernière étape, si tout se passe bien, vous pouvez créer l'entité réelle en copiant les données temporaires et les stocker.

L'autre est de créer Value Objectspour chaque étape et de stocker ensuite dans Cacheou Session. Ensuite, si tout se passe bien, vous pouvez créer votre objet Domaine à partir d'eux et l'enregistrer

Amila Silva
la source
1
Ce serait bien si les gens qui votent en bas donnent également leur raison.
Martin
Je n'ai pas voté contre vous, mais votre réponse n'a aucun rapport avec la question. L'OP demande comment créer l'assistant, tandis que vous répondez sur la façon de gérer la réponse à l'arrière.
Dementic
1
Je ne vote généralement pas, mais quand je le fais, je m'assure que c'est un vote positif :-)
Suhail Mumtaz Awan