ASP.NET MVC - Wie werden ModelState-Fehler in RedirectToAction beibehalten?

91

Ich habe die folgenden zwei Aktionsmethoden (für Fragen vereinfacht):

[HttpGet]
public ActionResult Create(string uniqueUri)
{
   // get some stuff based on uniqueuri, set in ViewData.  
   return View();
}

[HttpPost]
public ActionResult Create(Review review)
{
   // validate review
   if (validatedOk)
   {
      return RedirectToAction("Details", new { postId = review.PostId});
   }  
   else
   {
      ModelState.AddModelError("ReviewErrors", "some error occured");
      return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
   }   
}

Wenn die Validierung erfolgreich ist, leite ich auf eine andere Seite um (Bestätigung).

Wenn ein Fehler auftritt, muss ich die gleiche Seite mit dem Fehler anzeigen.

Wenn ich das tue return View(), wird der Fehler angezeigt, aber wenn ich es tue return RedirectToAction(wie oben), verliert es die Modellfehler.

Ich bin nicht überrascht von dem Problem und frage mich nur, wie ihr damit umgeht.

Ich könnte natürlich nur dieselbe Ansicht anstelle der Umleitung zurückgeben, aber ich habe Logik in der "Erstellen" -Methode, die die Ansichtsdaten auffüllt, die ich duplizieren müsste.

Irgendwelche Vorschläge?

RPM1984
quelle
10
Ich löse dieses Problem, indem ich das Post-Redirect-Get-Muster nicht für Validierungsfehler verwende. Ich benutze nur View (). Es ist absolut gültig, dies zu tun, anstatt durch ein paar Reifen zu springen - und Unordnung mit Ihrem Browserverlauf umzuleiten.
Jimmy Bogard
2
Extrahieren Sie zusätzlich zu den Aussagen von @JimmyBogard die Logik in der CreateMethode, die ViewData auffüllt, und rufen Sie sie in der CreateGET-Methode sowie im Zweig für fehlgeschlagene Validierung in der CreatePOST-Methode auf.
Russ Cam
1
Einverstanden, das Problem zu vermeiden, ist eine Möglichkeit, es zu lösen. Ich habe eine Logik, um CreateDinge in meiner Ansicht zu füllen . Ich habe sie einfach in eine Methode eingefügt populateStuff, die ich sowohl in the GETals auch in fail aufrufe POST.
Francois Joly
12
@JimmyBogard Ich bin anderer Meinung, wenn Sie zu einer Aktion posten und dann die Ansicht zurückgeben, auf die Sie stoßen, wenn der Benutzer auf Aktualisieren klickt, wird er gewarnt, dass er diesen Beitrag erneut initiieren möchte.
Der Muffin-Mann

Antworten:

50

Sie müssen dieselbe Instanz von Reviewfür Ihre HttpGetAktion haben. Dazu sollten Sie ein Objekt Review reviewin der temporären Variablen Ihrer HttpPostAktion speichern und es dann bei der HttpGetAktion wiederherstellen .

[HttpGet]
public ActionResult Create(string uniqueUri)
{
   //Restore
   Review review = TempData["Review"] as Review;            

   // get some stuff based on uniqueuri, set in ViewData.  
   return View(review);
}
[HttpPost]
public ActionResult Create(Review review)
{
   //Save your object
   TempData["Review"] = review;

   // validate review
   if (validatedOk)
   {
      return RedirectToAction("Details", new { postId = review.PostId});
   }  
   else
   {
      ModelState.AddModelError("ReviewErrors", "some error occured");
      return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
   }   
}

Wenn dies auch dann funktionieren soll, wenn der Browser nach der ersten Ausführung der HttpGetAktion aktualisiert wird , können Sie Folgendes tun:

  Review review = TempData["Review"] as Review;  
  TempData["Review"] = review;

Andernfalls ist das Objekt der Schaltfläche "Aktualisieren" reviewleer, da keine Daten vorhanden sind TempData["Review"].

kuncevic.dev
quelle
2
Ausgezeichnet. Und eine große +1 für die Erwähnung des Aktualisierungsproblems. Dies ist die vollständigste Antwort, also werde ich sie akzeptieren, danke ein paar. :)
RPM1984
8
Dies beantwortet die Frage im Titel nicht wirklich. ModelState wird nicht beibehalten, und dies hat Auswirkungen wie die Eingabe von HtmlHelpers, bei denen die Benutzereingabe nicht beibehalten wird. Dies ist fast eine Problemumgehung.
John Farrell
Am Ende habe ich getan, was @Wim in seiner Antwort vorgeschlagen hat.
RPM1984
17
@jfar, ich stimme zu, diese Antwort funktioniert nicht und behält den ModelState nicht bei. Wenn Sie es jedoch so ändern, dass es so etwas wie TempData["ModelState"] = ModelState; und mit wiederherstellt ModelState.Merge((ModelStateDictionary)TempData["ModelState"]);, dann würde es funktionieren
asgeo1
1
Könnten Sie nicht nur, return Create(uniqueUri)wenn die Validierung im POST fehlschlägt? Da ModelState-Werte Vorrang vor dem an die Ansicht übergebenen ViewModel haben, sollten die veröffentlichten Daten weiterhin erhalten bleiben.
Ajbeaven
83

Ich musste dieses Problem heute selbst lösen und bin auf diese Frage gestoßen.

Einige der Antworten sind nützlich (mithilfe von TempData), beantworten jedoch die vorliegende Frage nicht wirklich.

Der beste Rat, den ich gefunden habe, war in diesem Blog-Beitrag:

http://www.jefclaes.be/2012/06/persisting-model-state-when-using-prg.html

Verwenden Sie TempData grundsätzlich, um das ModelState-Objekt zu speichern und wiederherzustellen. Es ist jedoch viel sauberer, wenn Sie dies in Attribute abstrahieren.

Z.B

public class SetTempDataModelStateAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        base.OnActionExecuted(filterContext);         
        filterContext.Controller.TempData["ModelState"] = 
           filterContext.Controller.ViewData.ModelState;
    }
}

public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);
        if (filterContext.Controller.TempData.ContainsKey("ModelState"))
        {
            filterContext.Controller.ViewData.ModelState.Merge(
                (ModelStateDictionary)filterContext.Controller.TempData["ModelState"]);
        }
    }
}

Dann können Sie gemäß Ihrem Beispiel den ModelState wie folgt speichern / wiederherstellen:

[HttpGet]
[RestoreModelStateFromTempData]
public ActionResult Create(string uniqueUri)
{
    // get some stuff based on uniqueuri, set in ViewData.  
    return View();
}

[HttpPost]
[SetTempDataModelState]
public ActionResult Create(Review review)
{
    // validate review
    if (validatedOk)
    {
        return RedirectToAction("Details", new { postId = review.PostId});
    }  
    else
    {
        ModelState.AddModelError("ReviewErrors", "some error occured");
        return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
    }   
}

Wenn Sie das Modell auch in TempData weitergeben möchten (wie von bigb vorgeschlagen), können Sie dies auch weiterhin tun.

asgeo1
quelle
Danke dir. Wir haben etwas Ähnliches wie Ihren Ansatz implementiert. gist.github.com/ferventcoder/4735084
ferventcoder
Gute Antwort. Vielen Dank.
Mark Vickery
3
Diese Lösung ist der Grund, warum ich Stackoverflow verwende. Danke, Mann!
jugg1es
@ asgeo1 - großartige Lösung, aber ich bin auf ein Problem bei der Verwendung in Kombination mit der Wiederholung von Teilansichten gestoßen.
Josh
Schönes Beispiel dafür, wie man die einfache Lösung im Geiste von MVC sehr elegant macht. Sehr schön!
AHowgego
7

Warum nicht eine private Funktion mit der Logik in der Methode "Create" erstellen und diese Methode sowohl von der Get- als auch von der Post-Methode aufrufen und einfach View () zurückgeben?

Wim
quelle
Das ist eigentlich das, was ich getan habe - du hast meine Gedanken gelesen. +1 :)
RPM1984
1
Dies ist, was ich auch mache, nur anstatt eine private Funktion zu haben, lasse ich einfach meine POST-Methode die GET-Methode bei einem Fehler aufrufen (dh return Create(new { uniqueUri = ... });Ihre Logik bleibt trocken (ähnlich wie beim Aufrufen RedirectToAction), aber ohne die Probleme, die durch die Umleitung entstehen, wie z Verlieren Sie Ihren ModelState.
Daniel Liuzzi
1
@ DanielLiuzzi: Wenn Sie dies auf diese Weise tun, wird die URL nicht geändert. Sie beenden also mit einer URL wie "/ controller / create /".
Skorunka František
@ SkorunkaFrantišek Und genau das ist der Punkt. Die Frage lautet: Wenn ein Fehler auftritt, muss dieselbe Seite mit dem Fehler angezeigt werden. In diesem Zusammenhang ist es durchaus akzeptabel (und vorzugsweise IMO), dass sich die URL NICHT ändert, wenn dieselbe Seite angezeigt wird. Ein Vorteil dieses Ansatzes besteht auch darin, dass der Benutzer die Seite einfach aktualisieren kann, um das Formular erneut zu senden, wenn es sich bei dem fraglichen Fehler nicht um einen Validierungsfehler, sondern um einen Systemfehler handelt (z. B. DB-Zeitüberschreitung).
Daniel Liuzzi
4

ich könnte benutzen TempData["Errors"]

TempData werden über Aktionen hinweg übergeben, wobei Daten 1 Mal beibehalten werden.

Rob Waminal
quelle
4

Ich schlage vor, dass Sie die Ansicht zurückgeben und eine Duplizierung über ein Attribut für die Aktion vermeiden. Hier ist ein Beispiel für das Auffüllen zum Anzeigen von Daten. Mit Ihrer Methode zum Erstellen von Methoden können Sie etwas Ähnliches tun.

public class GetStuffBasedOnUniqueUriAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var filter = new GetStuffBasedOnUniqueUriFilter();

        filter.OnActionExecuting(filterContext);
    }
}


public class GetStuffBasedOnUniqueUriFilter : IActionFilter
{
    #region IActionFilter Members

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {

    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.Controller.ViewData["somekey"] = filterContext.RouteData.Values["uniqueUri"];
    }

    #endregion
}

Hier ist ein Beispiel:

[HttpGet, GetStuffBasedOnUniqueUri]
public ActionResult Create()
{
    return View();
}

[HttpPost, GetStuffBasedOnUniqueUri]
public ActionResult Create(Review review)
{
    // validate review
    if (validatedOk)
    {
        return RedirectToAction("Details", new { postId = review.PostId });
    }

    ModelState.AddModelError("ReviewErrors", "some error occured");
    return View(review);
}
CRice
quelle
Wie ist das eine schlechte Idee? Ich denke, das Attribut vermeidet die Verwendung einer anderen Aktion, da beide Aktionen das Attribut zum Laden in ViewData verwenden können.
CRice
1
Bitte werfen Sie einen Blick auf Post / Redirect / Get pattern: en.wikipedia.org/wiki/Post/Redirect/Get
DreamSonic
2
Dies wird normalerweise verwendet, nachdem die Modellvalidierung erfüllt ist, um zu verhindern, dass beim Aktualisieren weitere Beiträge in demselben Formular veröffentlicht werden. Wenn das Formular jedoch Probleme aufweist, muss es trotzdem korrigiert und erneut veröffentlicht werden. Diese Frage befasst sich mit der Behandlung von Modellfehlern.
CRice
Filter dienen zur Wiederverwendung von Code für Aktionen, insbesondere zum Einfügen von Elementen in ViewData. TempData ist nur eine Problemumgehung.
CRice
1
@ppumkin versuchen Sie vielleicht, mit Ajax zu posten, damit es Ihnen nicht schwer fällt, Ihre View-Server-Seite neu zu erstellen.
CRice
2

Ich habe eine Methode, die temporären Daten den Modellstatus hinzufügt. Ich habe dann eine Methode in meinem Basis-Controller, die temporäre Daten auf Fehler überprüft. Wenn sie vorhanden sind, werden sie wieder zu ModelState hinzugefügt.

Nick
quelle
1

Mein Szenario ist etwas komplizierter, da ich das PRG-Muster verwende, sodass sich mein ViewModel ("SummaryVM") in TempData befindet und auf meinem Zusammenfassungsbildschirm angezeigt wird. Auf dieser Seite befindet sich ein kleines Formular, mit dem Sie Informationen an eine andere Aktion senden können. Die Komplikation ist darauf zurückzuführen, dass der Benutzer einige Felder in SummaryVM auf dieser Seite bearbeiten muss.

Summary.cshtml enthält die Validierungszusammenfassung, die die von uns erstellten ModelState-Fehler abfängt.

@Html.ValidationSummary()

Mein Formular muss jetzt auf eine HttpPost-Aktion für Summary () POSTEN. Ich habe ein weiteres sehr kleines ViewModel, um bearbeitete Felder darzustellen, und die Modellbindung wird mir diese zur Verfügung stellen.

Die neue Form:

@using (Html.BeginForm("Summary", "MyController", FormMethod.Post))
{
    @Html.Hidden("TelNo") @* // Javascript to update this *@

und die Aktion ...

[HttpPost]
public ActionResult Summary(EditedItemsVM vm)

Hier mache ich eine Validierung und stelle eine schlechte Eingabe fest, daher muss ich mit den Fehlern zur Seite "Zusammenfassung" zurückkehren. Dafür verwende ich TempData, das eine Umleitung überlebt. Wenn es kein Problem mit den Daten gibt, ersetze ich das SummaryVM-Objekt durch eine Kopie (aber die bearbeiteten Felder wurden natürlich geändert) und führe dann eine RedirectToAction ("NextAction") durch.

// Telephone number wasn't in the right format
List<string> listOfErrors = new List<string>();
listOfErrors.Add("Telephone Number was not in the correct format. Value supplied was: " + vm.TelNo);
TempData["SummaryEditedErrors"] = listOfErrors;
return RedirectToAction("Summary");

Die Aktion "Summary Controller", bei der alles beginnt, sucht nach Fehlern in den Tempdata und fügt sie dem Modellstatus hinzu.

[HttpGet]
[OutputCache(Duration = 0)]
public ActionResult Summary()
{
    // setup, including retrieval of the viewmodel from TempData...


    // And finally if we are coming back to this after a failed attempt to edit some of the fields on the page,
    // load the errors stored from TempData.
        List<string> editErrors = new List<string>();
        object errData = TempData["SummaryEditedErrors"];
        if (errData != null)
        {
            editErrors = (List<string>)errData;
            foreach(string err in editErrors)
            {
                // ValidationSummary() will see these
                ModelState.AddModelError("", err);
            }
        }
VictorySaber
quelle
1

Microsoft hat die Möglichkeit zum Speichern komplexer Datentypen in TempData entfernt, sodass die vorherigen Antworten nicht mehr funktionieren. Sie können nur einfache Typen wie Zeichenfolgen speichern. Ich habe die Antwort von @ asgeo1 so geändert, dass sie wie erwartet funktioniert.

public class SetTempDataModelStateAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        base.OnActionExecuted(filterContext);

        var controller = filterContext.Controller as Controller;
        var modelState = controller?.ViewData.ModelState;
        if (modelState != null)
        {
            var listError = modelState.Where(x => x.Value.Errors.Any())
                .ToDictionary(m => m.Key, m => m.Value.Errors
                .Select(s => s.ErrorMessage)
                .FirstOrDefault(s => s != null));
            controller.TempData["KEY HERE"] = JsonConvert.SerializeObject(listError);
        }
    }
}


public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);

        var controller = filterContext.Controller as Controller;
        var tempData = controller?.TempData?.Keys;
        if (controller != null && tempData != null)
        {
            if (tempData.Contains("KEY HERE"))
            {
                var modelStateString = controller.TempData["KEY HERE"].ToString();
                var listError = JsonConvert.DeserializeObject<Dictionary<string, string>>(modelStateString);
                var modelState = new ModelStateDictionary();
                foreach (var item in listError)
                {
                    modelState.AddModelError(item.Key, item.Value ?? "");
                }

                controller.ViewData.ModelState.Merge(modelState);
            }
        }
    }
}

Von hier aus können Sie bei Bedarf einfach die erforderlichen Datenanmerkungen zu einer Controller-Methode hinzufügen.

[RestoreModelStateFromTempDataAttribute]
[HttpGet]
public async Task<IActionResult> MethodName()
{
}


[SetTempDataModelStateAttribute]
[HttpPost]
public async Task<IActionResult> MethodName()
{
    ModelState.AddModelError("KEY HERE", "ERROR HERE");
}
Alex Marchant
quelle
Funktioniert perfekt!. Die Antwort wurde bearbeitet, um einen kleinen Klammerfehler beim Einfügen des Codes zu beheben.
VDWWD
0

Ich ziehe es vor, meinem ViewModel eine Methode hinzuzufügen, die die Standardwerte auffüllt:

public class RegisterViewModel
{
    public string FirstName { get; set; }
    public IList<Gender> Genders { get; set; }
    //Some other properties here ....
    //...
    //...

    ViewModelType PopulateDefaultViewData()
    {
        this.FirstName = "No body";
        this.Genders = new List<Gender>()
        {
            Gender.Male,
            Gender.Female
        };

        //Maybe other assinments here for other properties...
    }
}

Dann nenne ich es, wann immer ich die Originaldaten wie folgt brauche:

    [HttpGet]
    public async Task<IActionResult> Register()
    {
        var vm = new RegisterViewModel().PopulateDefaultViewValues();
        return View(vm);
    }

    [HttpPost]
    public async Task<IActionResult> Register(RegisterViewModel vm)
    {
        if (!ModelState.IsValid)
        {
            return View(vm.PopulateDefaultViewValues());
        }

        var user = await userService.RegisterAsync(
            email: vm.Email,
            password: vm.Password,
            firstName: vm.FirstName,
            lastName: vm.LastName,
            gender: vm.Gender,
            birthdate: vm.Birthdate);

        return Json("Registered successfully!");
    }
Mohammed Noureldin
quelle