Probleme mit dem mehrstufigen Registrierungsprozess in asp.net mvc (geteilte Ansichtsmodelle, einzelnes Modell)

117

Ich habe einen mehrstufigen Registrierungsprozess , der von einem einzelnen Objekt in der Domänenschicht unterstützt wird und dessen Validierungsregeln für Eigenschaften definiert sind.

Wie soll ich das Domänenobjekt überprüfen, wenn die Domäne auf mehrere Ansichten aufgeteilt ist und ich das Objekt beim Posten teilweise in der ersten Ansicht speichern muss?

Ich habe über die Verwendung von Sitzungen nachgedacht, aber das ist nicht möglich, da der Prozess langwierig und die Datenmenge hoch ist. Daher möchte ich keine Sitzung verwenden.

Ich habe darüber nachgedacht, alle Daten in einer relationalen In-Memory-Datenbank (mit demselben Schema wie die Hauptdatenbank) zu speichern und diese Daten dann in die Hauptdatenbank zu leeren, aber es sind Probleme aufgetreten, weil ich zwischen Diensten (in den Ansichten angefordert) routen sollte, die mit der arbeiten Hauptdatenbank und In-Memory-Datenbank.

Ich suche eine elegante und saubere Lösung (genauer gesagt eine bewährte Methode).

UPDATE UND Klarstellung:

@Darin Vielen Dank für Ihre nachdenkliche Antwort. Genau das habe ich bis jetzt getan. Übrigens habe ich eine Anfrage, die viele Anhänge enthält. Ich entwerfe Step2Viewbeispielsweise, welcher Benutzer Dokumente asynchron hochladen kann. Diese Anhänge sollten jedoch in einer Tabelle mit referenzieller Beziehung zu einer anderen Tabelle gespeichert werden, die zuvor in gespeichert worden sein sollte Step1View.

Daher sollte ich das Domänenobjekt in Step1(teilweise) speichern , aber ich kann nicht, da das gesicherte Core-Domänenobjekt, das teilweise dem ViewModel eines Step1 zugeordnet ist, nicht ohne Requisiten gespeichert werden kann, die von konvertiert stammen Step2ViewModel.

Jahan
quelle
@Jani, hast du jemals das Upload-Stück davon herausgefunden? Ich würde gerne dein Gehirn auswählen. Ich arbeite genau an diesem Thema.
Doug Chamberlain
1
Die Lösung in diesem Blog ist recht einfach und unkompliziert. Divs werden als "Schritte" verwendet, indem ihre Sichtbarkeit und unauffällige JQuery-Validierung geändert werden.
Dmitry Efimenko
Mögliches Duplikat von MVC3 Wizard-Beispiel-Apps (mehrstufig) (NO JQUERY)
jgauffin

Antworten:

229

Zunächst sollten Sie in Ihren Ansichten keine Domänenobjekte verwenden. Sie sollten Ansichtsmodelle verwenden. Jedes Ansichtsmodell enthält nur die Eigenschaften, die für die angegebene Ansicht erforderlich sind, sowie die für diese bestimmte Ansicht spezifischen Validierungsattribute. Wenn Sie also einen Assistenten für drei Schritte haben, bedeutet dies, dass Sie drei Ansichtsmodelle haben, eines für jeden Schritt:

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

    ...
}

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

    ...
}

und so weiter. Alle diese Ansichtsmodelle können von einem Hauptansichtsmodell des Assistenten unterstützt werden:

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

Dann könnten Sie Controller-Aktionen ausführen, die jeden Schritt des Assistentenprozesses rendern und den Hauptschritt WizardViewModelan die Ansicht übergeben. Wenn Sie sich im ersten Schritt der Controller-Aktion befinden, können Sie die Step1Eigenschaft initialisieren . Anschließend generieren Sie in der Ansicht das Formular, mit dem der Benutzer die Eigenschaften von Schritt 1 ausfüllen kann. Wenn das Formular gesendet wird, wendet die Controller-Aktion nur die Validierungsregeln für Schritt 1 an:

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

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

In der Ansicht von Schritt 2 können Sie jetzt den Html.Serialize-Helfer aus MVC-Futures verwenden, um Schritt 1 in ein verstecktes Feld innerhalb des Formulars zu serialisieren (eine Art ViewState, wenn Sie dies wünschen):

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

und innerhalb der POST-Aktion von Schritt 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);
}

Und so weiter, bis Sie zum letzten Schritt gelangen, in dem Sie WizardViewModelalle Daten ausgefüllt haben . Anschließend ordnen Sie das Ansichtsmodell Ihrem Domänenmodell zu und übergeben es zur Verarbeitung an die Serviceschicht. Die Serviceschicht führt möglicherweise alle Validierungsregeln selbst aus und so weiter ...

Es gibt auch eine andere Alternative: Verwenden Sie Javascript und setzen Sie alle auf dieselbe Seite. Es gibt viele JQuery-Plugins , die Assistentenfunktionen bieten ( Stepy ist eine nette). Im Grunde geht es darum, Divs auf dem Client anzuzeigen und auszublenden. In diesem Fall müssen Sie sich keine Sorgen mehr über den anhaltenden Zustand zwischen den Schritten machen.

Unabhängig davon, für welche Lösung Sie sich entscheiden, verwenden Sie immer Ansichtsmodelle und führen Sie die Validierung für diese Ansichtsmodelle durch. Solange Sie die Validierungsattribute für Datenanmerkungen in Ihre Domänenmodelle einfügen, werden Sie große Probleme haben, da Domänenmodelle nicht an Ansichten angepasst sind.


AKTUALISIEREN:

OK, aufgrund der zahlreichen Kommentare komme ich zu dem Schluss, dass meine Antwort nicht klar war. Und ich muss zustimmen. Lassen Sie mich versuchen, mein Beispiel weiter auszuarbeiten.

Wir könnten eine Schnittstelle definieren, die alle Schrittansichtsmodelle implementieren sollten (es ist nur eine Markierungsschnittstelle):

public interface IStepViewModel
{
}

Dann würden wir 3 Schritte für den Assistenten definieren, wobei jeder Schritt natürlich nur die Eigenschaften enthält, die er benötigt, sowie die relevanten Validierungsattribute:

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

Als nächstes definieren wir das Hauptansichtsmodell des Assistenten, das aus einer Liste von Schritten und einem aktuellen Schrittindex besteht:

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

Dann gehen wir weiter zum 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);
    }
}

Einige Anmerkungen zu diesem Controller:

  • Die Aktion "Index-POST" verwendet die [Deserialize]Attribute aus der Microsoft Futures-Bibliothek. Stellen Sie daher sicher, dass Sie MvcContribNuGet installiert haben . Aus diesem Grund sollten Ansichtsmodelle mit dem [Serializable]Attribut versehen werden
  • Die Index-POST-Aktion verwendet als Argument eine IStepViewModelSchnittstelle. Damit dies sinnvoll ist, benötigen wir einen benutzerdefinierten Modellordner.

Hier ist der zugehörige Modellordner:

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

Dieser Ordner verwendet ein spezielles verstecktes Feld namens StepType, das den konkreten Typ jedes Schritts enthält und das wir bei jeder Anfrage senden.

Dieser Modellordner wird registriert in Application_Start:

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

Das letzte fehlende Teil des Puzzles sind die Ansichten. Hier ist die Hauptansicht ~/Views/Wizard/Index.cshtml:

@using Microsoft.Web.Mvc
@model WizardViewModel

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

<h3>Step @(Model.CurrentStepIndex + 1) out of @Model.Steps.Count</h3>

@using (Html.BeginForm())
{
    @Html.Serialize("wizard", Model)

    @Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType())
    @Html.EditorFor(x => currentStep, null, "")

    if (Model.CurrentStepIndex > 0)
    {
        <input type="submit" value="Previous" name="prev" />
    }

    if (Model.CurrentStepIndex < Model.Steps.Count - 1)
    {
        <input type="submit" value="Next" name="next" />
    }
    else
    {
        <input type="submit" value="Finish" name="finish" />
    }
}

Und das ist alles, was Sie brauchen, damit dies funktioniert. Wenn Sie möchten, können Sie natürlich das Erscheinungsbild einiger oder aller Schritte des Assistenten personalisieren, indem Sie eine benutzerdefinierte Editorvorlage definieren. Machen wir es zum Beispiel für Schritt 2. Also definieren wir einen ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtmlTeil:

@model Step2ViewModel

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

So sieht die Struktur aus:

Geben Sie hier die Bildbeschreibung ein

Natürlich gibt es Raum für Verbesserungen. Die Aktion Index POST sieht aus wie s..t. Es ist zu viel Code darin. Eine weitere Vereinfachung würde darin bestehen, alle Infrastrukturelemente wie Index, aktuelle Indexverwaltung, Kopieren des aktuellen Schritts in den Assistenten usw. in einen anderen Modellordner zu verschieben. Damit wir endlich am Ende haben:

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

So sollten POST-Aktionen aussehen. Ich verlasse diese Verbesserung für das nächste Mal :-)

Darin Dimitrov
quelle
1
@Doug Chamberlain, ich verwende AutoMapper , um zwischen meinen Ansichtsmodellen und Domänenmodellen zu konvertieren.
Darin Dimitrov
1
@ Doug Chamberlain, siehe meine aktualisierte Antwort. Ich hoffe, es macht die Dinge etwas klarer als mein erster Beitrag.
Darin Dimitrov
20
+1 @Jani: Du musst Darin wirklich die 50 Punkte für diese Antwort geben. Es ist sehr umfassend. Und er hat es geschafft, die Notwendigkeit zu wiederholen, ViewModel und nicht Domain-Modelle zu verwenden ;-)
Tom Chantler
3
Ich kann das Deserialize-Attribut nirgendwo finden ... Auch auf der Codeplex-Seite von mvccontrib finde ich dieses 94fa6078a115 von Jeremy Skinner 1. August 2010 um 17:55 Uhr 0 Entfernen Sie den veralteten Deserialize-Ordner Was schlagen Sie mir vor?
Chuck Norris
2
Ich habe ein Problem gefunden, obwohl ich meine Ansichten nicht als Schritt 1, Schritt 2 usw. bezeichnet habe. Meine sind etwas aussagekräftiger, aber nicht alphabetisch. Also habe ich meine Modelle in die falsche Reihenfolge gebracht. Ich habe der IStepViewModel-Schnittstelle eine StepNumber-Eigenschaft hinzugefügt. Jetzt kann ich in der Initialize-Methode von WizardViewModel danach sortieren.
Jeff Reddy
13

Als Ergänzung zu Amit Baggas Antwort finden Sie unten, was ich getan habe. Auch wenn ich weniger elegant bin, finde ich diesen Weg einfacher als Darins Antwort.

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

Modelle:

 [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
quelle
11

Ich würde Ihnen empfehlen, den Status des vollständigen Prozesses auf dem Client mithilfe von Jquery beizubehalten.

Zum Beispiel haben wir einen dreistufigen Assistentenprozess.

  1. Dem Benutzer wird der Schritt 1 angezeigt, auf dem sich eine Schaltfläche mit der Bezeichnung "Weiter" befindet.
  2. Wenn Sie auf Weiter klicken, stellen wir eine Ajax-Anfrage und erstellen eine DIV mit dem Namen Step2 und laden den HTML-Code in diese DIV.
  3. Auf Schritt 3 haben wir eine Schaltfläche mit der Bezeichnung "Fertig" beim Klicken auf die Schaltfläche, um die Daten mit dem Aufruf $ .post zu veröffentlichen.

Auf diese Weise können Sie Ihr Domänenobjekt einfach direkt aus den Formularpostdaten erstellen. Falls die Daten fehlerhaft sind, geben Sie einen gültigen JSON zurück, der alle Fehlermeldungen enthält, und zeigen Sie sie in einem div an.

Bitte teilen Sie die Schritte

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

Das Obige ist nur eine Demonstration, die Ihnen hilft, das Endergebnis zu erzielen. Im letzten Schritt müssen Sie das Domänenobjekt erstellen und die korrekten Werte aus dem Assistentenobjekt und dem Speicher in die Datenbank einfügen.

Amit Bagga
quelle
Ja, das ist eine interessante Lösung, aber wir haben leider eine schlechte Internetverbindung auf der Client-Seite, und er / sie sollte uns eine Reihe von Dateien senden. Deshalb haben wir diese Lösung früher abgelehnt.
Jahan
Können Sie mir bitte mitteilen, wie viele Daten der Client hochladen wird?
Amit Bagga
Mehrere Dateien, fast zehn, jede fast 1 MB.
Jahan
5

Assistenten sind nur einfache Schritte zur Verarbeitung eines einfachen Modells. Es gibt keinen Grund, mehrere Modelle für einen Assistenten zu erstellen. Sie müssen lediglich ein einzelnes Modell erstellen und es zwischen Aktionen in einem einzelnen Controller übergeben.

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

Die obige Studentin ist einfach dumm, also ersetzen Sie Ihre Felder dort. Als nächstes beginnen wir mit einer einfachen Aktion, die unseren Assistenten initiiert.

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

Dies ruft die Ansicht "WizardStep1.cshtml" auf (wenn Sie also Rasiermesser verwenden). Sie können den Assistenten zum Erstellen von Vorlagen verwenden, wenn Sie möchten. Wir leiten den Beitrag lediglich zu einer anderen Aktion um.

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

Die Sache ist, dass wir dies in einer anderen Aktion veröffentlichen werden; die WizardStep2-Aktion

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

In dieser Aktion prüfen wir, ob unser Modell gültig ist, und senden es in diesem Fall an unsere Ansicht WizardStep2.cshtml. Andernfalls senden wir es mit den Validierungsfehlern an Schritt 1 zurück. In jedem Schritt senden wir es an den nächsten Schritt, validieren diesen Schritt und fahren fort. Einige versierte Entwickler könnten nun sagen, dass wir nicht zwischen solchen Schritten wechseln können, wenn wir [Erforderliche] Attribute oder andere Datenanmerkungen zwischen den Schritten verwenden. Und Sie hätten Recht, also entfernen Sie die Fehler bei Elementen, die noch überprüft werden müssen. Wie unten.

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

Schließlich würden wir das Modell einmal im Datenspeicher speichern. Dies verhindert auch, dass ein Benutzer, der einen Assistenten startet, ihn jedoch nicht beendet, unvollständige Daten nicht in der Datenbank speichert.

Ich hoffe, Sie finden diese Methode zum Implementieren eines Assistenten viel einfacher zu verwenden und zu warten als jede der zuvor genannten Methoden.

Danke fürs Lesen.

Darroll
quelle
Haben Sie dies in einer Komplettlösung, die ich ausprobieren kann? Danke
Mpora
5

Ich wollte meine eigene Art des Umgangs mit diesen Anforderungen teilen. Ich wollte SessionState überhaupt nicht verwenden, noch wollte ich, dass es clientseitig behandelt wird, und die Serialisierungsmethode erfordert MVC-Futures, die ich nicht in mein Projekt aufnehmen wollte.

Stattdessen habe ich einen HTML-Helper erstellt, der alle Eigenschaften des Modells durchläuft und für jedes ein benutzerdefiniertes verstecktes Element generiert. Wenn es sich um eine komplexe Eigenschaft handelt, wird sie rekursiv ausgeführt.

In Ihrem Formular werden sie zusammen mit den neuen Modelldaten bei jedem "Assistenten" -Schritt an die Steuerung gesendet.

Ich habe das für MVC 5 geschrieben.

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

Jetzt können Sie für alle Schritte Ihres "Assistenten" dasselbe Basismodell verwenden und die Modelleigenschaften "Schritt 1,2,3" mithilfe eines Lambda-Ausdrucks an den @ Html.HiddenClassFor-Helfer übergeben.

Sie können sogar bei jedem Schritt einen Zurück-Button haben, wenn Sie möchten. Haben Sie einfach eine Zurück-Schaltfläche in Ihrem Formular, die es mithilfe des formaction-Attributs an eine StepNBack-Aktion auf dem Controller sendet. Nicht im folgenden Beispiel enthalten, sondern nur eine Idee für Sie.

Sowieso ist hier ein grundlegendes Beispiel:

Hier ist dein MODELL

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

Hier ist dein 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);
    }
}

Hier sind deine ANSICHTEN

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

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

Schritt 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
quelle
1
Könnten Sie Ihre Lösung weiter erläutern, indem Sie das Ansichtsmodell und den Controller bereitstellen?
Tyler Durden
2

Hinzufügen weiterer Informationen aus @ Darins Antwort.

Was ist, wenn Sie für jeden Schritt einen eigenen Entwurfsstil haben und jeden in einer separaten Teilansicht beibehalten möchten, oder wenn Sie für jeden Schritt mehrere Eigenschaften haben?

Während der Verwendung haben Html.EditorForwir die Einschränkung, die Teilansicht zu verwenden.

Erstellen Sie 3 Teilansichten unter dem SharedOrdner:Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

Der Kürze halber poste ich gerade die erste Patialansicht. Andere Schritte sind die gleichen wie Darins Antwort.

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

    }
}

Wenn es eine bessere Lösung gibt, kommentieren Sie diese bitte, um andere zu informieren.

Shaijut
quelle
-9

Eine Möglichkeit besteht darin, einen Satz identischer Tabellen zu erstellen, in denen die in jedem Schritt gesammelten Daten gespeichert werden. Wenn alles gut geht, können Sie im letzten Schritt die reale Entität erstellen, indem Sie die temporären Daten kopieren und speichern.

Eine andere ist, Value Objectsfür jeden Schritt zu erstellen und dann in Cacheoder zu speichern Session. Wenn alles gut geht, können Sie Ihr Domain-Objekt daraus erstellen und speichern

Amila Silva
quelle
1
Wäre schön, wenn Leute, die abstimmen, auch ihren Grund angeben.
Martin
Ich habe Sie nicht abgelehnt, aber Ihre Antwort ist für die Frage völlig irrelevant. Das OP fragt, wie der Assistent erstellt werden soll, während Sie antworten, wie die Antwort im Hintergrund behandelt werden soll.
Dementic
1
Ich stimme normalerweise nicht ab, aber wenn ich das tue, stelle ich sicher, dass es positiv bewertet wird :-)
Suhail Mumtaz Awan