Wie kann ich ModelState mit RedirectToAction pflegen?

72

Wie kann ich das Ergebnis einer anderen Aktion zurückgeben oder den Benutzer zu einer anderen Aktion verschieben, wenn in meinem ModelState ein Fehler auftritt, ohne meine ModelState-Informationen zu verlieren?

Das Szenario ist; Die Aktion "Löschen" akzeptiert einen POST aus einem DELETE-Formular, das von meiner Indexaktion / -ansicht gerendert wurde. Wenn beim Löschen ein Fehler auftritt, möchte ich den Benutzer zurück zur Indexaktion / -ansicht verschieben und die Fehler anzeigen, die von der Löschaktion im gespeichert werden ViewData.ModelState. Wie kann dies in ASP.NET MVC durchgeführt werden?

[AcceptVerbs(HttpVerbs.Post | HttpVerbs.Delete)]
public ActionResult Delete([ModelBinder(typeof(RdfUriBinder))] RdfUri graphUri)
{
    if (!ModelState.IsValid)
        return Index(); //this needs to be replaced with something that works :)

    return RedirectToAction("Index");
}
Eric Schoonover
quelle

Antworten:

100

Speichern Sie Ihre Ansichtsdaten in TempData und rufen Sie sie von dort in Ihrer Indexaktion ab, falls vorhanden.

   ...
   if (!ModelState.IsValid)
       TempData["ViewData"] = ViewData;

   RedirectToAction( "Index" );
}

 public ActionResult Index()
 {
     if (TempData["ViewData"] != null)
     {
         ViewData = (ViewDataDictionary)TempData["ViewData"];
     }

     ...
 }

[BEARBEITEN] Ich habe die Online-Quelle auf MVC überprüft und es scheint, dass die ViewData im Controller einstellbar sind. Daher ist es wahrscheinlich am einfachsten, alle ViewData, einschließlich des ModelState, in die Index-Aktion zu übertragen.

Tvanfosson
quelle
ViewData.ModelState hat keinen Setter.
Eric Schoonover
2
Das funktioniert ... Danke! Ich wünschte, es gäbe einen saubereren Weg, dies zu tun ... vielleicht ist dies sauber, aber es scheint eine häufige Aufgabe zu sein, die Teil einer RedirectToAction-Überschreibung sein sollte oder so.
Eric Schoonover
6
Sie können es in Aktionsfilter umgestalten, wie hier vorgeschlagen (siehe Punkt
Tomas Aschan
5
Beachten Sie Folgendes: TempData verwendet standardmäßig Session. Wenn Ihre Sitzung vom StateServer oder SqlServer unterstützt wird, treten bei dieser Technik Probleme beim Serialisieren der ViewData auf, da der ViewDataDictionary-Typ nicht als serialisierbar markiert ist.
Jon Schoning
1
Da ViewDataDictionary nicht serialisierbar ist, ist es nicht möglich, ViewData zwischen Anforderungen in TempData beizubehalten, wenn die "Sitzung" etwas anderes als "In-Process" ist, wodurch diese Lösung nicht mit Webfarmen, Sitzungsstatusdiensten und Cookie-temporären Datenanbietern kompatibel ist , etc :(
Novox
40

Verwenden Sie Aktionsfilter (PRG-Muster) (so einfach wie das Verwenden von Attributen).

Erwähnt hier und hier .

Bob
quelle
3
Beste Antwort für dieses Problem IMO.
Jean-Francois
2
Ja das ist die Antwort. Ich
Chris S
ModelState.Merge () war das, wonach ich gesucht habe. Toller Link. +1
Ryanulit
Der Link zu Rashids Blog ist von ASP.NET verschwunden. Das scheint seltsam. Doch seine Lösung und der entsprechende Code wird ebenfalls diskutiert hier .
Marc L.
11

Bitte beachten Sie, dass die Lösung von tvanfosson nicht immer funktioniert, obwohl sie in den meisten Fällen in Ordnung sein sollte.

Das Problem bei dieser speziellen Lösung besteht darin, dass Sie, wenn Sie bereits über ViewData oder ModelState verfügen, alles mit dem Status der vorherigen Anforderung überschreiben. Beispielsweise kann die neue Anforderung einige Modellstatusfehler aufweisen, die sich auf ungültige Parameter beziehen, die an die Aktion übergeben werden. Diese werden jedoch ausgeblendet, da sie überschrieben werden.

Eine andere Situation, in der es möglicherweise nicht wie erwartet funktioniert, ist, wenn Sie einen Aktionsfilter hatten, der einige ViewData- oder ModelState-Fehler initialisiert hat. Wieder würden sie durch diesen Code überschrieben.

Wir suchen nach Lösungen für ASP.NET MVC, mit denen Sie den Status aus den beiden Anforderungen einfacher zusammenführen können. Bleiben Sie also auf dem Laufenden.

Danke, Eilon

Eilon
quelle
Hey Eilon, ist die Antwort von @ bob (aus dem Blog von Kazi Manzur Rashid) immer noch der beste Weg, dies zu tun, oder empfiehlt das MVC-Team derzeit eine andere Methode?
Patrick McDonald
1
@PatrickMcDonald Es gibt nichts Neues in MVC, von dem ich mir vorstellen kann, dass es dieses Problem lösen würde. Ich würde jedoch davor warnen, alle ViewData vollständig zu überschreiben und stattdessen selektiver zu entscheiden, was von der vorherigen Anforderung in die neue Anforderung kopiert wird.
Eilon
ModelState hat eine MergeFunktion.
Chris Haines
5

Falls dies für jemanden nützlich ist, habe ich die von @bob empfohlene Lösung mit PRG verwendet:

siehe Punkt 13 -> Link .

Ich hatte das zusätzliche Problem, dass Nachrichten in der VeiwBag an die Ansicht übergeben und manuell aus TempData in den Controller-Aktionen überprüft / geladen wurden, wenn a ausgeführt wurde RedirectToAction("Action"). Um zu vereinfachen (und auch wartbar zu machen), habe ich diesen Ansatz leicht erweitert, um auch andere Daten zu überprüfen und zu speichern / laden. Meine Handlungsmethoden sahen ungefähr so ​​aus:

 [AcceptVerbs(HttpVerbs.Post)]
 [ExportModelStateToTempData]
 public ActionResult ChangePassword(ProfileViewModel pVM) {
      bool result = MyChangePasswordCode(pVM.ChangePasswordViewModel);
      if (result) {
           ViewBag.Message = "Password change success";
      else {
           ModelState.AddModelError("ChangePassword", "Some password error");
      }
      return RedirectToAction("Index");
    }

Und meine Indexaktion:

[ImportModelStateFromTempData]
public ActionResult Index() {
    ProfileViewModel pVM = new ProfileViewModel { //setup }
    return View(pVM);
}

Der Code in den Aktionsfiltern:

// Following best practices as listed here for storing / restoring model data:
// http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx#prg
public abstract class ModelStateTempDataTransfer : ActionFilterAttribute {
    protected static readonly string Key = typeof(ModelStateTempDataTransfer).FullName;
}

::

public class ExportModelStateToTempData : ModelStateTempDataTransfer {
    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        //Only export when ModelState is not valid
        if (!filterContext.Controller.ViewData.ModelState.IsValid) {
            //Export if we are redirecting
            if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult)) {
                filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState;
            }
        }
        // Added to pull message from ViewBag
        if (!string.IsNullOrEmpty(filterContext.Controller.ViewBag.Message)) {
            filterContext.Controller.TempData["Message"] = filterContext.Controller.ViewBag.Message;
        }

        base.OnActionExecuted(filterContext);
    }
}

::

public class ImportModelStateFromTempData : ModelStateTempDataTransfer {
    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;

        if (modelState != null) {
            //Only Import if we are viewing
            if (filterContext.Result is ViewResult) {
                filterContext.Controller.ViewData.ModelState.Merge(modelState);
            } else {
                //Otherwise remove it.
                filterContext.Controller.TempData.Remove(Key);
            }
        }
        // Restore Viewbag message
        if (!string.IsNullOrEmpty((string)filterContext.Controller.TempData["Message"])) {
            filterContext.Controller.ViewBag.Message = filterContext.Controller.TempData["Message"];
        }

        base.OnActionExecuted(filterContext);
    }
}

Mir ist klar, dass meine Änderungen hier eine ziemlich offensichtliche Erweiterung dessen sind, was bereits mit dem ModelState durch den Code @ the link von @bob gemacht wurde - aber ich musste über diesen Thread stolpern, bevor ich überhaupt daran dachte, ihn auf diese Weise zu handhaben.

Matthew
quelle
Vielen Dank, Sie müssen nur die Zeile von ImportModelStateFromTempData für Teilansichten bearbeiten >> if (filterContext.Result ist ViewResult || filterContext.Result ist PartialViewResult)
k4st0r42
0

Bitte spießen Sie mich nicht für diese Antwort auf. Es ist ein legitimer Vorschlag.

Verwenden Sie AJAX

Der Code zum Verwalten von ModelState ist kompliziert und weist (wahrscheinlich?) Auf andere Probleme in Ihrem Code hin.

Sie können ganz einfach Ihren eigenen AJAX-Javascript-Code rollen. Hier ist ein Skript, das ich benutze:

https://gist.github.com/jesslilly/5f646ef29367ad2b0228e1fa76d6bdcc#file-ajaxform

(function ($) {

    $(function () {

        // For forms marked with data-ajax="#container",
        // on submit,
        // post the form data via AJAX
        // and if #container is specified, replace the #container with the response.
        var postAjaxForm = function (event) {

            event.preventDefault(); // Prevent the actual submit of the form.

            var $this = $(this);
            var containerId = $this.attr("data-ajax");
            var $container = $(containerId);
            var url = $this.attr('action');

            console.log("Post ajax form to " + url + " and replace html in " + containerId);

            $.ajax({
                type: "POST",
                url: url,
                data: $this.serialize()
            })
                .done(function (result) {
                    if ($container) {
                        $container.html(result);
                        // re-apply this event since it would have been lost by the form getting recreated above.
                        var $newForm = $container.find("[data-ajax]");
                        $newForm.submit(postAjaxForm);
                        $newForm.trigger("data-ajax-done");
                    }
                })
                .fail(function (error) {
                    alert(error);
                });
        };
        $("[data-ajax]").submit(postAjaxForm);
    });

})(jQuery);
Jess
quelle
-3

Vielleicht versuchen

return View("Index");

anstatt

return Index();
Ty.
quelle
Das funktioniert nicht, da die Logik in der Indexaktion nicht ausgeführt wird. Es wird lediglich versucht, das aktuelle Modell mithilfe der Indexansicht zu rendern.
Eric Schoonover
Möchten Sie die Modellfehler nicht einfach in derselben Ansicht anzeigen, aus der Sie gepostet haben? Was machen Sie in der Index-Aktion, die ausgeführt werden muss, wenn Modellfehler vorliegen? Ich gebe nur View ("ViewName", Modell) zurück, wenn es Fehler gibt und es gut funktioniert.
Ty.
Nein, ich möchte zur Indexaktion umleiten und die Ansicht an die von dieser Aktion generierten Daten sowie an den ModelState binden, der durch die fehlgeschlagene Löschaktion definiert wurde.
Eric Schoonover
Ich verstehe die negativen Stimmen nicht, weil es eigentlich eine gute Antwort ist und Sie Ihre "Logik" aus dem Index zu einem Dienst bringen können, der das Modell für Ihre Indexansicht erstellt. Und danach: Geben Sie View ("Index", modelObjectThatProducedByYourService) zurück. Der ModelState wird beibehalten, um Fehler in der Indexansicht anzuzeigen.
jannagy02