problemi relativi al processo di registrazione in più passaggi in asp.net mvc (modelli di visualizzazione divisi, modello singolo)


117

Ho un processo di registrazione in più fasi , supportato da un singolo oggetto nel livello di dominio , che ha regole di convalida definite sulle proprietà.

Come devo convalidare l'oggetto del dominio quando il dominio è suddiviso in molte visualizzazioni e devo salvare parzialmente l'oggetto nella prima visualizzazione quando viene pubblicato?

Ho pensato di utilizzare le sessioni, ma non è possibile perché il processo è lungo e la quantità di dati è elevata, quindi non voglio utilizzare la sessione.

Ho pensato di salvare tutti i dati in un db in memoria relazionale (con lo stesso schema del db principale) e quindi scaricare quei dati nel db principale ma sono sorti problemi perché dovrei instradare tra i servizi (richiesti nelle viste) che lavorano con il database principale e database in memoria.

Sto cercando una soluzione elegante e pulita (più precisamente una best practice).

AGGIORNAMENTO E Chiarimento:

@Darin Grazie per la tua premurosa risposta, è esattamente quello che ho fatto fino ad ora. Ma incidentalmente ho una richiesta che ha molti allegati, Step2Viewad esempio progetto un esempio quale utente può caricare documenti in esso in modo asincrono, ma quegli allegati dovrebbero essere salvati in una tabella con relazione referenziale a un'altra tabella che avrebbe dovuto essere salvata prima in Step1View.

Quindi dovrei salvare l'oggetto dominio in Step1(parzialmente), ma non posso, perché l'oggetto Core Domain supportato che è mappato parzialmente su ViewModel di Step1 non può essere salvato senza oggetti di scena che provengono da convertiti Step2ViewModel.


@Jani, hai mai capito il pezzo da caricare di questo? Vorrei scegliere il tuo cervello. Sto lavorando a questo esatto problema.
Doug Chamberlain

1
La soluzione in questo blog è abbastanza semplice e diretta. Utilizza i div come "passaggi" modificandone la visibilità e la convalida jquery non invadente.
Dmitry Efimenko

Risposte:


229

Per prima cosa non dovresti usare alcun oggetto di dominio nelle tue viste. Dovresti usare i modelli di visualizzazione. Ogni modello di vista conterrà solo le proprietà richieste dalla vista data così come gli attributi di convalida specifici per questa vista data. Quindi, se hai 3 passaggi guidati questo significa che avrai 3 modelli di visualizzazione, uno per ogni passaggio:

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

    ...
}

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

    ...
}

e così via. Tutti questi modelli di visualizzazione potrebbero essere supportati da un modello di visualizzazione guidata principale:

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

quindi potresti avere azioni del controller che riproducono ogni passaggio del processo di procedura guidata e passano il principale WizardViewModelalla vista. Quando sei al primo passaggio all'interno dell'azione del controller puoi inizializzare la Step1proprietà. Quindi all'interno della vista genererai il modulo che consente all'utente di compilare le proprietà del passaggio 1. Quando il modulo viene inviato, l'azione del controller applicherà le regole di convalida solo per il passaggio 1:

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

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

Ora all'interno della visualizzazione del passaggio 2 è possibile utilizzare l' helper Html.Serialize dai futures MVC per serializzare il passaggio 1 in un campo nascosto all'interno del modulo (una sorta di ViewState se lo si desidera):

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

e all'interno dell'azione POST del passaggio 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 così via fino ad arrivare all'ultimo passaggio in cui avrai WizardViewModelriempito tutti i dati. Quindi mapperai il modello di visualizzazione al tuo modello di dominio e lo passerai al livello di servizio per l'elaborazione. Il livello di servizio potrebbe eseguire autonomamente le regole di convalida e così via ...

C'è anche un'altra alternativa: usare javascript e mettere tutto sulla stessa pagina. Ci sono molti plugin jquery là fuori che forniscono funzionalità di procedura guidata ( Stepy è carino). Fondamentalmente si tratta di mostrare e nascondere i div sul client, nel qual caso non è più necessario preoccuparsi dello stato persistente tra i passaggi.

Indipendentemente dalla soluzione scelta, utilizza sempre i modelli di visualizzazione ed esegui la convalida su tali modelli di visualizzazione. Finché manterrai gli attributi di convalida dell'annotazione dei dati sui tuoi modelli di dominio, ti sarà molto difficile perché i modelli di dominio non sono adattati alle visualizzazioni.


AGGIORNARE:

OK, a causa dei numerosi commenti, traggo la conclusione che la mia risposta non era chiara. E devo essere d'accordo. Quindi lasciatemi provare a elaborare ulteriormente il mio esempio.

Potremmo definire un'interfaccia che tutti i modelli di visualizzazione dei passaggi dovrebbero implementare (è solo un'interfaccia marker):

public interface IStepViewModel
{
}

quindi definiremmo 3 passaggi per la procedura guidata in cui ogni passaggio conterrebbe ovviamente solo le proprietà richieste, nonché gli attributi di convalida pertinenti:

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

successivamente definiamo il modello di visualizzazione della procedura guidata principale che consiste in un elenco di passaggi e un indice dei passaggi correnti:

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

Quindi passiamo al controller:

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

Un paio di osservazioni su questo controller:

  • L'azione Index POST usa gli [Deserialize]attributi della libreria Microsoft Futures, quindi assicurati di aver installato MvcContribNuGet. Questo è il motivo per cui i modelli di visualizzazione dovrebbero essere decorati con l' [Serializable]attributo
  • L'azione Index POST prende come argomento IStepViewModelun'interfaccia, quindi perché ciò abbia senso abbiamo bisogno di un raccoglitore di modelli personalizzato.

Ecco il raccoglitore di modelli associato:

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

Questo raccoglitore utilizza uno speciale campo nascosto chiamato StepType che conterrà il tipo concreto di ogni passaggio e che invieremo ad ogni richiesta.

Questo raccoglitore di modelli sarà registrato in Application_Start:

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

L'ultimo pezzo mancante del puzzle sono le viste. Ecco la ~/Views/Wizard/Index.cshtmlvista 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" />
    }
}

E questo è tutto ciò di cui hai bisogno per farlo funzionare. Ovviamente, se lo desideri, puoi personalizzare l'aspetto di alcuni o tutti i passaggi della procedura guidata definendo un modello di editor personalizzato. Ad esempio, facciamolo per il passaggio 2. Quindi definiamo un ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtmlparziale:

@model Step2ViewModel

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

Ecco come appare la struttura:

inserisci qui la descrizione dell'immagine

Ovviamente c'è spazio per miglioramenti. L'azione Index POST è simile a s..t. C'è troppo codice in esso. Un'ulteriore semplificazione comporterebbe lo spostamento di tutte le cose dell'infrastruttura come l'indice, la gestione corrente dell'indice, la copia del passaggio corrente nella procedura guidata, ... in un altro raccoglitore di modelli. Così finalmente ci ritroviamo con:

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

che è più come dovrebbero apparire le azioni POST. Lascio questo miglioramento per la prossima volta :-)


1
@Doug Chamberlain, utilizzo AutoMapper per convertire tra i miei modelli di visualizzazione e modelli di dominio.
Darin Dimitrov

1
@Doug Chamberlain, guarda la mia risposta aggiornata. Spero che renda le cose un po 'più chiare rispetto al mio post iniziale.
Darin Dimitrov

20
+1 @ Jani: devi davvero dare a Darin i 50 punti per questa risposta. È molto completo. Ed è riuscito a ribadire la necessità di utilizzare ViewModel e non i modelli Domain ;-)
Tom Chantler

3
Non riesco a trovare l'attributo Deserialize da nessuna parte ... Anche nella pagina codeplex di mvccontrib trovo questo 94fa6078a115 di Jeremy Skinner 1 agosto 2010 alle 17:55 0 Rimuovi il raccoglitore Deserialize deprecato Cosa mi suggerisci di fare?
Chuck Norris

2
Ho riscontrato un problema mentre non nominavo le mie viste Step1, Step2, ecc ... Le mie sono chiamate in modo più significativo, ma non alfabetico. Quindi, ho finito per mettere i miei modelli nell'ordine sbagliato. Ho aggiunto una proprietà StepNumber all'interfaccia IStepViewModel. Ora posso ordinare in base a questo nel metodo Initialize di WizardViewModel.
Jeff Reddy

13

Per completare la risposta di Amit Bagga troverai di seguito cosa ho fatto. Anche se meno elegante, trovo questo modo più semplice della risposta di Darin.

Controller:

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

Modelli :

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

11

Ti suggerirei di mantenere lo stato di Complete Process sul client utilizzando Jquery.

Ad esempio, abbiamo un processo guidato in tre fasi.

  1. L'utente si presenta con il passaggio 1 su cui è presente un pulsante etichettato "Avanti"
  2. Facendo clic su Avanti, effettuiamo una richiesta Ajax e creiamo un DIV chiamato Step2 e carichiamo l'HTML in quel DIV.
  3. Sul passaggio 3 abbiamo un pulsante etichettato "Finito" facendo clic sul pulsante inserisci i dati utilizzando la chiamata $ .post.

In questo modo puoi creare facilmente il tuo oggetto di dominio direttamente dai dati del modulo post e nel caso in cui i dati contengano errori restituisci un JSON valido contenente tutti i messaggi di errore e visualizzali in un div.

Si prega di dividere i passaggi

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

Quanto sopra è solo una dimostrazione che ti aiuterà a raggiungere il risultato finale. Nella fase finale è necessario creare l'oggetto dominio e popolare i valori corretti dall'oggetto procedura guidata e archiviare nel database.


Sì, è una soluzione interessante, ma sfortunatamente abbiamo una connessione Internet scadente dal lato client e lui / lei dovrebbe inviarci un sacco di file. quindi abbiamo rifiutato quella soluzione in precedenza.
Jahan

Potete farmi sapere il volume di dati che il client caricherà.
Amit Bagga

Diversi file, quasi dieci, ciascuno di quasi 1 MB.
Jahan

5

Le procedure guidate sono solo semplici passaggi nell'elaborazione di un semplice modello. Non c'è motivo di creare più modelli per una procedura guidata. Tutto quello che faresti è creare un singolo modello e passarlo tra le azioni in un unico controller.

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

Il suddetto coed è stupido semplice quindi sostituisci i tuoi campi lì dentro. Successivamente iniziamo con una semplice azione che avvia la nostra procedura guidata.

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

Questo richiama la vista "WizardStep1.cshtml (se si utilizza il rasoio che è). È possibile utilizzare la procedura guidata di creazione del modello se lo si desidera. Reindirizzeremo il post a un'azione diversa.

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

La cosa da notare è che lo pubblicheremo in un'azione diversa; l'azione WizardStep2

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

In questa azione controlliamo se il nostro modello è valido, e in tal caso lo inviamo alla nostra vista WizardStep2.cshtml altrimenti lo rimandiamo al passaggio uno con gli errori di convalida. In ogni passaggio lo inviamo al passaggio successivo, convalidiamo quel passaggio e andiamo avanti. Ora alcuni sviluppatori esperti potrebbero dire che non possiamo muoverci tra passaggi come questo se utilizziamo attributi [Richiesti] o altre annotazioni di dati tra i passaggi. E avresti ragione, quindi rimuovi gli errori sugli elementi che devono ancora essere controllati. come sotto.

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

Infine salveremo il modello una volta nell'archivio dati. Ciò impedisce anche a un utente che avvia una procedura guidata ma non la termina di non salvare dati incompleti nel database.

Spero che tu trovi questo metodo di implementazione di una procedura guidata molto più facile da usare e mantenere rispetto a qualsiasi metodo menzionato in precedenza.

Grazie per aver letto.


hai questo in una soluzione completa che posso provare? Grazie
mpora

5

Volevo condividere il mio modo di gestire questi requisiti. Non volevo affatto usare SessionState, né volevo che fosse gestito dal lato client, e il metodo serialize richiede MVC Futures che non volevo includere nel mio progetto.

Invece ho creato un Helper HTML che itererà attraverso tutte le proprietà del modello e genererà un elemento nascosto personalizzato per ognuna. Se si tratta di una proprietà complessa, verrà eseguita in modo ricorsivo su di essa.

Nel tuo modulo verranno inviati al controller insieme ai dati del nuovo modello in ogni fase della "procedura guidata".

L'ho scritto per 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));
        }
    }
}

Ora per tutti i passaggi della "procedura guidata" è possibile utilizzare lo stesso modello di base e passare le proprietà del modello "Passaggio 1,2,3" nell'helper @ Html.HiddenClassFor utilizzando un'espressione lambda.

Puoi anche avere un pulsante Indietro in ogni passaggio, se lo desideri. Basta avere un pulsante Indietro nel modulo che lo pubblicherà in un'azione StepNBack sul controller utilizzando l'attributo formaction. Non incluso nell'esempio seguente ma solo un'idea per te.

Comunque ecco un esempio di base:

Ecco il tuo MODELLO

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

Ecco il tuo CONTROLLER

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

Ecco le tue VISUALIZZAZIONI

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

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

1
Potresti chiarire ulteriormente la tua soluzione fornendo il modello di visualizzazione e il controller?
Tyler Durden

2

Aggiunta di ulteriori informazioni dalla risposta di @ Darin.

Cosa succede se si dispone di uno stile di progettazione separato per ogni passaggio e si desidera mantenerli in una vista parziale separata o se si hanno più proprietà per ogni passaggio?

Durante l'utilizzo Html.EditorForabbiamo limitazioni per utilizzare la visualizzazione parziale.

Crea 3 viste parziali nella Sharedcartella denominata:Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

Per brevità ho appena postato la prima vista patologica, gli altri passaggi sono gli stessi della risposta di 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 esiste una soluzione migliore, commenta per farlo sapere agli altri.


-9

Un'opzione è creare una serie di tabelle identiche che memorizzeranno i dati raccolti in ogni passaggio. Quindi nell'ultimo passaggio se tutto va bene puoi creare l'entità reale copiando i dati temporanei e memorizzandola.

Altro è creare Value Objectsper ogni passaggio e memorizzare quindi in Cacheo Session. Quindi, se tutto va bene, puoi creare il tuo oggetto Dominio da loro e salvarlo


1
Sarebbe bello se le persone che votano giù dessero anche la loro ragione.
Martin

Non ti ho votato giù, ma la tua risposta è completamente irrilevante per la domanda. L'OP ti chiede come creare la procedura guidata, mentre tu rispondi su come gestire la risposta nella parte posteriore.
Demenza

1
Di solito non voto, ma quando lo faccio, mi assicuro che sia positivo :-)
Suhail Mumtaz Awan
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.