Asp.net MVC ModelState.Clear

116

Kann mir jemand eine kurze Definition der Rolle von ModelState in Asp.net MVC geben (oder einen Link zu einer). Insbesondere muss ich wissen, in welchen Situationen es notwendig oder wünschenswert ist, anzurufen ModelState.Clear().

Etwas offenes Ende huh ... Entschuldigung, ich denke, es könnte helfen, wenn ich Ihnen sage, was ich akut mache:

Ich habe eine Aktion zum Bearbeiten auf einem Controller namens "Seite". Wenn ich das Formular zum Ändern der Details der Seite zum ersten Mal sehe, wird alles gut geladen (Bindung an ein "MyCmsPage" -Objekt). Dann klicke ich auf eine Schaltfläche, die einen Wert für eines der Felder des MyCmsPage-Objekts generiert ( MyCmsPage.SeoTitle). Es generiert eine Geldstrafe und aktualisiert das Objekt. Anschließend gebe ich das Aktionsergebnis mit dem neu geänderten Seitenobjekt zurück und erwarte, dass das relevante Textfeld (gerendert mit <%= Html.TextBox("seoTitle", page.SeoTitle)%>) aktualisiert wird. Leider wird der Wert des alten Modells angezeigt, das geladen wurde.

Ich habe es mit verwendet, ModelState.Clear()aber ich muss wissen, warum / wie es funktioniert hat, damit ich es nicht nur blind mache.

PageController:

[AcceptVerbs("POST")]
public ActionResult Edit(MyCmsPage page, string submitButton)
{
    // add the seoTitle to the current page object
    page.GenerateSeoTitle();

    // why must I do this?
    ModelState.Clear();

    // return the modified page object
     return View(page);
 }

Aspx:

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyCmsPage>" %>
....
        <div class="c">
            <label for="seoTitle">
                Seo Title</label>
            <%= Html.TextBox("seoTitle", page.SeoTitle)%>
            <input type="submit" value="Generate Seo Title" name="submitButton" />
        </div>
Herr Grok
quelle
Noob AspMVC, wenn es alte Daten zwischenspeichern will, was bringt es dann, dem Benutzer wieder ein Modell zu geben: @ Ich hatte das gleiche Problem, vielen Dank bro
deadManN

Antworten:

135

Ich denke, es ist ein Fehler in MVC. Ich hatte heute stundenlang mit diesem Problem zu kämpfen.

Angesichts dessen:

public ViewResult SomeAction(SomeModel model) 
{
    model.SomeString = "some value";
    return View(model); 
}

Die Ansicht wird mit dem Originalmodell gerendert, wobei die Änderungen ignoriert werden. Also dachte ich, vielleicht mag es mich nicht, dasselbe Modell zu verwenden, also habe ich es so versucht:

public ViewResult SomeAction(SomeModel model) 
{
    var newModel = new SomeModel { SomeString = "some value" };
    return View(newModel); 
}

Und dennoch wird die Ansicht mit dem Originalmodell gerendert. Was seltsam ist, wenn ich einen Haltepunkt in die Ansicht setze und das Modell untersuche, hat es den geänderten Wert. Der Antwortstrom hat jedoch die alten Werte.

Schließlich entdeckte ich die gleiche Arbeit wie Sie:

public ViewResult SomeAction(SomeModel model) 
{
    var newModel = new SomeModel { SomeString = "some value" };
    ModelState.Clear();
    return View(newModel); 
}

Funktioniert wie erwartet.

Ich denke nicht, dass dies ein "Feature" ist, oder?

Tim Scott
quelle
32
Habe fast genau das Gleiche getan wie du. Ich habe herausgefunden, dass dies kein Fehler ist. Es ist beabsichtigt: Ein Bug? EditorFor und DisplayFor zeigen nicht den gleichen Wert an und die HTML-Helfer von ASP.NET MVC rendern den falschen Wert
Metro Smurf
8
Mann, ich habe schon 2 Stunden damit gekämpft. Vielen Dank für die Veröffentlichung dieser Antwort!
Andrey Agibalov
37
Das ist immer noch wahr und viele Leute, einschließlich mir, verlieren dadurch viel Zeit. Fehler oder von Natur aus ist es mir egal, es ist "unerwartet".
Proviste
7
Ich stimme @Proviste zu, ich hoffe, dass dieses "Feature" in Zukunft entfernt wird
Ben
8
Ich habe gerade vier Stunden damit verbracht. Hässlich.
Brian MacKay
45

Aktualisieren:

  • Dies ist kein Fehler.
  • Bitte hören Sie auf, View()von einer POST-Aktion zurückzukehren. Verwenden Sie stattdessen PRG und leiten Sie zu einem GET um, wenn die Aktion erfolgreich ist.
  • Wenn Sie sind eine Rückkehr View()von einer POST - Aktion, tun es für Formularvalidierung, und tun es die Art und Weise MVC konzipiert in Helfer die eingebaute verwenden. Wenn Sie es so machen, sollten Sie es nicht brauchen.Clear()
  • Wenn Sie diese Aktion verwenden, um Ajax für ein SPA zurückzugeben , verwenden Sie einen Web-API-Controller und vergessen ModelStateSie dies, da Sie ihn sowieso nicht verwenden sollten.

Alte Antwort:

ModelState in MVC wird hauptsächlich verwendet, um den Status eines Modellobjekts weitgehend in Bezug darauf zu beschreiben, ob dieses Objekt gültig ist oder nicht. Dieses Tutorial sollte viel erklären.

Im Allgemeinen sollten Sie den ModelState nicht löschen müssen, da er von der MVC-Engine für Sie verwaltet wird. Das manuelle Löschen kann zu unerwünschten Ergebnissen führen, wenn versucht wird, die Best Practices für die MVC-Validierung einzuhalten.

Es scheint, dass Sie versuchen, einen Standardwert für den Titel festzulegen. Dies sollte erfolgen, wenn das Modellobjekt instanziiert wird (Domänenschicht irgendwo oder im Objekt selbst - parameterloser Ctor), und zwar bei der Aktion get so, dass es beim ersten Mal auf die Seite oder vollständig auf dem Client (über Ajax oder etwas anderes) gelangt. so dass es so aussieht, als ob der Benutzer es eingegeben hat und es mit der Sammlung der veröffentlichten Formulare zurückkommt. Einige , wie Sie Ihr Ansatz , diesen Wertes der Zugabe auf dem Empfang einer Formen Sammlung (in der POST - Aktion // Edit) verursachen dieses bizarre Verhalten , das in einer Folge könnte .Clear() erscheinen , an der Arbeit für Sie. Vertrauen Sie mir - Sie wollen nicht die klare verwenden. Probieren Sie eine der anderen Ideen aus.

Matt Kocaj
quelle
1
Hilft mir zwar dabei, meine Serviceschicht ein wenig zu überdenken (stöhnen, aber danke), aber wie bei vielen Dingen im Netz neigt es stark zum Standpunkt, ModelState für die Validierung zu verwenden.
Herr Grok
Der Frage wurden weitere Informationen hinzugefügt, um zu zeigen, warum ich besonders an ModelState.Clear () interessiert bin und warum meine Anfrage gestellt wurde
Mr Grok
5
Ich kaufe dieses Argument nicht wirklich, um die Rückgabe von View (...) von einer [HttpPost] -Funktion zu beenden. Wenn Sie Inhalte über Ajax POSTEN und dann das Dokument mit der resultierenden PartialView aktualisieren, wurde der MVC ModelState als falsch angezeigt. Die einzige Problemumgehung, die ich gefunden habe, besteht darin, sie in der Controller-Methode zu löschen.
Aaron Hudon
@AaronHudon PRG ist ziemlich gut etabliert.
Matt Kocaj
Wenn ich mit einem AJAX-Aufruf POST, kann ich dann zu einer GET-Aktion umleiten und eine modellgefüllte Ansicht zurückgeben, wie es das OP möchte, alles asynchron?
MyiEye
17

Wenn Sie einen Wert für ein einzelnes Feld löschen möchten, fand ich die folgende Technik hilfreich.

ModelState.SetModelValue("Key", new ValueProviderResult(null, string.Empty, CultureInfo.InvariantCulture));

Hinweis: Ändern Sie "Schlüssel" in den Namen des Feldes, das Sie zurücksetzen möchten.

Carl Saunders
quelle
Ich weiß nicht, warum das bei mir anders funktioniert hat (MVC4 vielleicht)? Aber ich musste danach auch model.Key = "" machen. Beide Zeilen sind erforderlich.
TTT
Ich möchte Ihnen zum Kommentar @PeterGluck ein Kompliment machen. Es ist besser, als den vollständigen Modellstatus zu löschen (da ich Fehler in einigen Feldern habe, die ich behalten möchte).
Tjab
6

Nun, der ModelState enthält im Grunde genommen den aktuellen Status des Modells in Bezug auf die Validierung

ModelErrorCollection: Stellen Sie die Fehler dar, wenn das Modell versucht, die Werte zu binden. Ex.

TryUpdateModel();
UpdateModel();

oder wie ein Parameter im ActionResult

public ActionResult Create(Person person)

ValueProviderResult : Halten Sie die Details der versuchten Bindung an das Modell fest. Ex. AttemptedValue, Culture, RawValue .

Die Methode clear () muss mit Vorsicht angewendet werden, da dies zu unerwarteten Ergebnissen führen kann. Und Sie verlieren einige nette Eigenschaften des ModelState wie AttemptedValue. Dies wird von MVC im Hintergrund verwendet, um die Formularwerte im Fehlerfall neu zu füllen.

ModelState["a"].Value.AttemptedValue
JOBG
quelle
1
hmmm ... Vielleicht bekomme ich das Problem so, wie es aussieht. Ich habe den Wert der Model.SeoTitle-Eigenschaft überprüft und er hat sich geändert, der versuchte Wert jedoch nicht. Es sieht so aus, als würde der Wert eingefügt, als ob ein Fehler auf der Seite vorliegt, obwohl es keinen gibt (habe das ModelState-Wörterbuch überprüft und es gibt keine Fehler).
Herr Grok
6

Ich hatte eine Instanz, in der ich das Modell eines summierten Formulars aktualisieren wollte und aus Leistungsgründen nicht auf Aktion umleiten wollte. Frühere Werte von ausgeblendeten Feldern wurden in meinem aktualisierten Modell beibehalten - was zu einer Vielzahl von Problemen führte!.

Einige Codezeilen identifizierten bald die Elemente in ModelState, die ich (nach der Validierung) entfernen wollte, sodass die neuen Werte in der folgenden Form verwendet wurden:

while (ModelState.FirstOrDefault(ms => ms.Key.ToString().StartsWith("SearchResult")).Value != null)
{
    ModelState.Remove(ModelState.FirstOrDefault(ms => ms.Key.ToString().StartsWith("SearchResult")));
}
stevieg
quelle
5

Nun, viele von uns scheinen davon gebissen worden zu sein, und obwohl der Grund dafür sinnvoll ist, brauchte ich einen Weg, um sicherzustellen, dass der Wert meines Modells angezeigt wurde und nicht ModelState.

Einige haben vorgeschlagen ModelState.Remove(string key), aber es ist nicht offensichtlich, was keysein sollte, insbesondere für verschachtelte Modelle. Hier sind einige Methoden, die ich entwickelt habe, um dies zu unterstützen.

Die RemoveStateForMethode nimmt ein ModelStateDictionary, ein Modell und einen Ausdruck für die gewünschte Eigenschaft und entfernt sie. HiddenForModelkann in Ihrer Ansicht verwendet werden, um ein verstecktes Eingabefeld zu erstellen, bei dem nur der Wert aus dem Modell verwendet wird, indem zuerst der ModelState-Eintrag entfernt wird. (Dies könnte leicht für die anderen Hilfserweiterungsmethoden erweitert werden).

/// <summary>
/// Returns a hidden input field for the specified property. The corresponding value will first be removed from
/// the ModelState to ensure that the current Model value is shown.
/// </summary>
public static MvcHtmlString HiddenForModel<TModel, TProperty>(this HtmlHelper<TModel> helper,
    Expression<Func<TModel, TProperty>> expression)
{
    RemoveStateFor(helper.ViewData.ModelState, helper.ViewData.Model, expression);
    return helper.HiddenFor(expression);
}

/// <summary>
/// Removes the ModelState entry corresponding to the specified property on the model. Call this when changing
/// Model values on the server after a postback, to prevent ModelState entries from taking precedence.
/// </summary>
public static void RemoveStateFor<TModel, TProperty>(this ModelStateDictionary modelState, TModel model,
    Expression<Func<TModel, TProperty>> expression)
{
    var key = ExpressionHelper.GetExpressionText(expression);

    modelState.Remove(key);
}

Anruf von einem Controller wie folgt:

ModelState.RemoveStateFor(model, m => m.MySubProperty.MySubValue);

oder aus einer solchen Sicht:

@Html.HiddenForModel(m => m.MySubProperty.MySubValue)

Es wird verwendet System.Web.Mvc.ExpressionHelper, um den Namen der ModelState-Eigenschaft abzurufen.

Tobias J.
quelle
1
Sehr schön! Behalten Sie dies für die ExpressionHelper-Funktionalität im Auge.
Gerard ONeill
4

Ich wollte einen Wert aktualisieren oder zurücksetzen, wenn er nicht vollständig validiert wurde, und bin auf dieses Problem gestoßen.

Die einfache Antwort, ModelState.Remove, ist problematisch, denn wenn Sie Helfer verwenden, kennen Sie den Namen nicht wirklich (es sei denn, Sie halten sich an die Namenskonvention). Es sei denn, Sie erstellen eine Funktion, mit der sowohl Ihr benutzerdefinierter Helfer als auch Ihr Controller einen Namen abrufen können.

Diese Funktion sollte als Option im Helper implementiert worden sein, wobei dies standardmäßig nicht der Fall ist. Wenn Sie jedoch möchten, dass die nicht akzeptierte Eingabe erneut angezeigt wird, können Sie dies einfach sagen.

Aber zumindest verstehe ich das Problem jetzt;).

Gerard ONeill
quelle
Ich musste genau das tun; Siehe meine Methoden, die ich unten gepostet habe und die mir geholfen haben, Remove()den richtigen Schlüssel zu finden.
Tobias J
0

Habe es am Ende. Mein benutzerdefinierter ModelBinder, der nicht registriert wurde und dies tut:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

myCmsPage.SeoTitle = (!String.IsNullOrEmpty(frm["seoTitle"])) ? frm["seoTitle"] : null;

Etwas, das die Standardmodellbindung tat, muss das Problem verursacht haben. Ich weiß nicht was, aber mein Problem ist zumindest behoben, nachdem mein benutzerdefinierter Modellordner registriert wurde.

Herr Grok
quelle
Nun, ich habe keine Erfahrung mit einem benutzerdefinierten ModelBinder, der Standard entspricht meinen bisherigen Anforderungen =).
JOBG
0

Im Allgemeinen ist es an der Zeit, Ihren Ansatz zu überdenken, wenn Sie gegen Standardpraktiken kämpfen. In diesem Fall das Verhalten von ModelState. Wenn Sie beispielsweise nach einem POST keinen Modellstatus wünschen, ziehen Sie eine Umleitung zum get in Betracht.

[HttpPost]
public ActionResult Edit(MyCmsPage page, string submitButton)
{
    if (ModelState.IsValid) {
        SomeRepository.SaveChanges(page);
        return RedirectToAction("GenerateSeoTitle",new { page.Id });
    }
    return View(page);
}

public ActionResult GenerateSeoTitle(int id) {
     var page = SomeRepository.Find(id);
     page.GenerateSeoTitle();
     return View("Edit",page);
}

BEARBEITET, um Kulturkommentar zu beantworten:

Hier ist, was ich verwende, um eine multikulturelle MVC-Anwendung zu handhaben. Zuerst die Unterklassen des Routenhandlers:

public class SingleCultureMvcRouteHandler : MvcRouteHandler {
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        var culture = requestContext.RouteData.Values["culture"].ToString();
        if (string.IsNullOrWhiteSpace(culture))
        {
            culture = "en";
        }
        var ci = new CultureInfo(culture);
        Thread.CurrentThread.CurrentUICulture = ci;
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
        return base.GetHttpHandler(requestContext);
    }
}

public class MultiCultureMvcRouteHandler : MvcRouteHandler
{
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        var culture = requestContext.RouteData.Values["culture"].ToString();
        if (string.IsNullOrWhiteSpace(culture))
        {
            culture = "en";
        }
        var ci = new CultureInfo(culture);
        Thread.CurrentThread.CurrentUICulture = ci;
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
        return base.GetHttpHandler(requestContext);
    }
}

public class CultureConstraint : IRouteConstraint
{
    private string[] _values;
    public CultureConstraint(params string[] values)
    {
        this._values = values;
    }

    public bool Match(HttpContextBase httpContext,Route route,string parameterName,
                        RouteValueDictionary values, RouteDirection routeDirection)
    {

        // Get the value called "parameterName" from the 
        // RouteValueDictionary called "value"
        string value = values[parameterName].ToString();
        // Return true is the list of allowed values contains 
        // this value.
        return _values.Contains(value);

    }

}

public enum Culture
{
    es = 2,
    en = 1
}

Und so verkabele ich die Routen. Nach dem Erstellen der Routen stelle ich meinem Subagenten (example.com/subagent1, example.com/subagent2 usw.) den Kulturcode voran. Wenn Sie nur die Kultur benötigen, entfernen Sie einfach den Subagenten aus den Routenhandlern und Routen.

    public static void RegisterRoutes(RouteCollection routes)
    {

        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.IgnoreRoute("Content/{*pathInfo}");
        routes.IgnoreRoute("Cache/{*pathInfo}");
        routes.IgnoreRoute("Scripts/{pathInfo}.js");
        routes.IgnoreRoute("favicon.ico");
        routes.IgnoreRoute("apple-touch-icon.png");
        routes.IgnoreRoute("apple-touch-icon-precomposed.png");

        /* Dynamically generated robots.txt */
        routes.MapRoute(
            "Robots.txt", "robots.txt",
            new { controller = "Robots", action = "Index", id = UrlParameter.Optional }
        );

        routes.MapRoute(
             "Sitemap", // Route name
             "{subagent}/sitemap.xml", // URL with parameters
             new { subagent = "aq", controller = "Default", action = "Sitemap"},  new[] { "aq3.Controllers" } // Parameter defaults
        );

        routes.MapRoute(
             "Rss Feed", // Route name
             "{subagent}/rss", // URL with parameters
             new { subagent = "aq", controller = "Default", action = "RSS"},  new[] { "aq3.Controllers" } // Parameter defaults
        );

        /* remap wordpress tags to mvc blog posts */
        routes.MapRoute(
            "Tag", "tag/{title}",
            new { subagent = "aq", controller = "Default", action = "ThreeOhOne", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler(); ;

        routes.MapRoute(
            "Custom Errors", "Error/{*errorType}",
            new { controller = "Error", action = "Index", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        );

        /* dynamic images not loaded from content folder */
        routes.MapRoute(
            "Stock Images",
            "{subagent}/Images/{*filename}",
            new { subagent = "aq", controller = "Image", action = "Show", id = UrlParameter.Optional, culture = "en"},  new[] { "aq3.Controllers" }
        );

        /* localized routes follow */
        routes.MapRoute(
            "Localized Images",
            "Images/{*filename}",
            new { subagent = "aq", controller = "Image", action = "Show", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        routes.MapRoute(
            "Blog Posts",
            "Blog/{*postname}",
            new { subagent = "aq", controller = "Blog", action = "Index", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        routes.MapRoute(
            "Office Posts",
            "Office/{*address}",
            new { subagent = "aq", controller = "Offices", action = "Address", id = UrlParameter.Optional }, new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        routes.MapRoute(
             "Default", // Route name
             "{controller}/{action}/{id}", // URL with parameters
             new { subagent = "aq", controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "aq3.Controllers" } // Parameter defaults
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        foreach (System.Web.Routing.Route r in routes)
        {
            if (r.RouteHandler is MultiCultureMvcRouteHandler)
            {
                r.Url = "{subagent}/{culture}/" + r.Url;
                //Adding default culture 
                if (r.Defaults == null)
                {
                    r.Defaults = new RouteValueDictionary();
                }
                r.Defaults.Add("culture", Culture.en.ToString());

                //Adding constraint for culture param
                if (r.Constraints == null)
                {
                    r.Constraints = new RouteValueDictionary();
                }
                r.Constraints.Add("culture", new CultureConstraint(Culture.en.ToString(), Culture.es.ToString()));
            }
        }

    }
B2K
quelle
Sie haben sehr Recht, wenn Sie die POST REDIRECT-Praxis vorschlagen. Tatsächlich mache ich dies für fast jede Post-Aktion. Ich hatte jedoch ein ganz besonderes Bedürfnis: Ich habe ein Filterformular oben auf der Seite, das zunächst mit get eingereicht wurde. Aber ich stieß auf ein Problem mit einem nicht gebundenen Datumsfeld und stellte dann fest, dass GET-Anfragen die Kultur nicht herumtragen (ich verwende Französisch für meine App), sodass ich die Anfrage auf POST umstellen musste, um mein Datum erfolgreich zu binden. Dann kam dieses Problem, ich stecke sie ein wenig fest ..
Souhaieb Besbes
@ SouhaiebBesbes Meine Updates zeigen, wie ich mit Kultur umgehe.
B2K
@ SouhaiebBesbes vielleicht ein bisschen einfacher wäre es, deine Kultur in TempData zu speichern. Siehe stackoverflow.com/questions/12422930/…
B2K
0

Nun, das schien auf meiner Rasiermesserseite zu funktionieren und machte noch nie einen Rundgang zur CS-Datei. Dies ist ein alter HTML-Weg. Es könnte nützlich sein.

<input type="reset" value="Reset">
JustJohn
quelle