Asp.net MVC ModelState.Clear

116

Quelqu'un peut-il me donner une définition succincte du rôle de ModelState dans Asp.net MVC (ou un lien vers un). En particulier, j'ai besoin de savoir dans quelles situations il est nécessaire ou souhaitable d'appeler ModelState.Clear().

Peu ouvert, hein ... désolé, je pense que cela pourrait aider si vous disiez ce que je fais réellement:

J'ai une action d'édition sur un contrôleur appelé "Page". Quand je vois pour la première fois le formulaire pour modifier les détails de la page, tout se charge correctement (liaison à un objet "MyCmsPage"). Ensuite, je clique sur un bouton qui génère une valeur pour l'un des champs de l'objet MyCmsPage ( MyCmsPage.SeoTitle). Il génère bien et met à jour l'objet, puis je renvoie le résultat de l'action avec l'objet de page nouvellement modifié et j'attends que la zone de texte appropriée (rendue à l'aide de <%= Html.TextBox("seoTitle", page.SeoTitle)%>) soit mise à jour ... mais hélas, elle affiche la valeur de l'ancien modèle chargé.

Je l'ai contourné en utilisant ModelState.Clear()mais j'ai besoin de savoir pourquoi / comment cela a fonctionné, donc je ne le fais pas aveuglément.

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>
Monsieur Grok
la source
Noob AspMVC, s'il veut mettre en cache d'anciennes données, alors quel est l'intérêt de redonner le modèle à l'utilisateur: @ j'ai eu le même problème, merci beaucoup bro
deadManN

Réponses:

135

Je pense que c'est un bogue dans MVC. J'ai lutté avec ce problème pendant des heures aujourd'hui.

Compte tenu de ceci:

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

La vue est rendue avec le modèle d'origine, ignorant les modifications. Alors j'ai pensé, peut-être que ça ne m'aime pas d'utiliser le même modèle, alors j'ai essayé comme ça:

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

Et toujours la vue est rendue avec le modèle d'origine. Ce qui est étrange, c'est que lorsque je mets un point d'arrêt dans la vue et que j'examine le modèle, il a la valeur modifiée. Mais le flux de réponse a les anciennes valeurs.

Finalement, j'ai découvert le même travail que vous avez fait:

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

Fonctionne comme prévu.

Je ne pense pas que ce soit une «fonctionnalité», n'est-ce pas?

Tim Scott
la source
33
J'ai juste fait exactement la même chose que toi. Mais j'ai découvert que ce n'était pas un bug. C'est par conception: un bug? EditorFor et DisplayFor n'affiche pas la même valeur et les Helpers Html d'ASP.NET MVC rendent la mauvaise valeur
Metro Smurf
8
Mec, j'ai déjà passé 2 heures à me battre avec. Merci d'avoir publié cette réponse!
Andrey Agibalov
37
c'est toujours vrai et beaucoup de gens, moi y compris, perdent beaucoup de temps à cause de cela. bug ou par conception, je m'en fiche, c'est "inattendu".
Proviste
7
Je suis d'accord avec @Proviste, j'espère que cette "fonctionnalité" sera supprimée dans le futur
Ben
8
Je viens de passer quatre heures là-dessus. Laid.
Brian MacKay
46

Mettre à jour:

  • Ce n'est pas un bug.
  • Veuillez arrêter de revenir View()d'une action POST. Utilisez plutôt PRG et redirigez vers un GET si l'action réussit.
  • Si vous êtes retournerez un View()d'une action POST, faites - le pour la validation du formulaire, et faire le chemin MVC est conçu en utilisant le haut dans les aides. Si vous le faites de cette façon, vous ne devriez pas avoir besoin d'utiliser.Clear()
  • Si vous utilisez cette action pour renvoyer ajax pour un SPA , utilisez un contrôleur d'API Web et oubliez cela ModelStatecar vous ne devriez pas l'utiliser de toute façon.

Ancienne réponse:

ModelState dans MVC est principalement utilisé pour décrire l'état d'un objet de modèle en grande partie par rapport à savoir si cet objet est valide ou non. Ce tutoriel devrait en expliquer beaucoup.

En règle générale, vous ne devriez pas avoir besoin d'effacer le ModelState car il est géré par le moteur MVC pour vous. Le supprimer manuellement peut entraîner des résultats indésirables lorsque vous essayez de respecter les meilleures pratiques de validation MVC.

Il semble que vous essayez de définir une valeur par défaut pour le titre. Cela devrait être fait lorsque l'objet du modèle est instancié (couche de domaine quelque part ou dans l'objet lui-même - ctor sans paramètre), sur l'action get de telle sorte qu'il descende à la page la 1ère fois ou complètement sur le client (via ajax ou quelque chose) afin qu'il apparaisse comme si l'utilisateur l'a entré et il revient avec la collection de formulaires publiée. Certains comment ce comportement est à l' origine bizarre qui pourrait se traduire par une votre approche d'ajouter cette valeur à la réception d'une collection de formes (dans l'action POST // Edit) .Clear() apparaissant au travail pour vous. Croyez-moi, vous ne voulez pas utiliser le clair. Essayez l'une des autres idées.

Matt Kocaj
la source
1
Cela m'aide à repenser un peu ma couche de services (gémissement mais merci), mais comme avec beaucoup de choses sur le net, cela penche fortement vers le point de vue de l'utilisation de ModelState pour la validation.
Mr Grok
Ajout de plus d'informations à la question pour montrer pourquoi je suis particulièrement intéressé par ModelState.Clear () et la raison de ma requête
Mr Grok
5
Je n'achète pas vraiment cet argument pour arrêter de renvoyer View (...) à partir d'une fonction [HttpPost]. Si vous publiez du contenu via ajax, puis mettez à jour le document avec le PartialView résultant, le MVC ModelState s'est avéré incorrect. La seule solution de contournement que j'ai trouvée est de l'effacer dans la méthode du contrôleur.
Aaron Hudon
@AaronHudon PRG est assez bien établi.
Matt Kocaj
Si je POST avec un appel AJAX, puis-je rediriger vers une action GET et renvoyer une vue remplie de modèle comme le souhaite l'OP, le tout de manière asynchrone?
MyiEye
17

Si vous souhaitez effacer une valeur pour un champ individuel, j'ai trouvé la technique suivante utile.

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

Remarque: remplacez «Clé» par le nom du champ que vous souhaitez réinitialiser.

Carl Saunders
la source
Je ne sais pas pourquoi cela a fonctionné différemment pour moi (MVC4 peut-être)? Mais j'ai dû aussi faire model.Key = "" par la suite. Les deux lignes sont obligatoires.
TTT
Je voudrais vous féliciter pour le commentaire de suppression @PeterGluck. C'est mieux que d'effacer l'état du modèle complet (puisque j'ai des erreurs sur certains champs que j'aimerais conserver).
Tjab
6

Eh bien, le ModelState contient essentiellement l'état actuel du modèle en termes de validation, il tient

ModelErrorCollection: représente les erreurs lorsque le modèle essaie de lier les valeurs. ex.

TryUpdateModel();
UpdateModel();

ou comme un paramètre dans ActionResult

public ActionResult Create(Person person)

ValueProviderResult : contient les détails sur la tentative de liaison au modèle. ex. AttemptedValue, Culture, RawValue .

La méthode Clear () doit être utilisée avec prudence car elle peut conduire à des résultats non détectés. Et vous perdrez quelques belles propriétés du ModelState comme AttemptedValue, ceci est utilisé par MVC en arrière-plan pour repeupler les valeurs du formulaire en cas d'erreur.

ModelState["a"].Value.AttemptedValue
JOBG
la source
1
hmmm ... C'est peut-être là que je reçois le problème à première vue. J'ai inspecté la valeur de la propriété Model.SeoTitle et elle a changé mais pas la valeur tentée. On dirait que la valeur est collée comme s'il y avait une erreur sur la page même s'il n'y en avait pas (j'ai vérifié le dictionnaire ModelState et il n'y a pas d'erreurs).
Mr Grok
6

J'ai eu une instance où je voulais mettre à jour le modèle d'un formulaire résumé, et je ne voulais pas «rediriger vers l'action» pour des raisons de performance. Les valeurs précédentes des champs masqués étaient conservées sur mon modèle mis à jour - provoquant toutes sortes de problèmes !.

Quelques lignes de code ont rapidement identifié les éléments dans ModelState que je voulais supprimer (après validation), donc les nouvelles valeurs ont été utilisées sous la forme: -

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

Eh bien, beaucoup d'entre nous semblent avoir été mordus par cela, et bien que la raison pour laquelle cela se produit soit logique, j'avais besoin d'un moyen de m'assurer que la valeur de mon modèle était affichée, et non ModelState.

Certains ont suggéré ModelState.Remove(string key), mais ce qui keydevrait être n'est pas évident , en particulier pour les modèles imbriqués. Voici quelques méthodes que j'ai mises au point pour vous aider.

La RemoveStateForméthode prendra un ModelStateDictionary, un modèle et une expression pour la propriété souhaitée et la supprimera. HiddenForModelpeut être utilisé dans votre vue pour créer un champ d'entrée masqué en utilisant uniquement la valeur du modèle, en supprimant d'abord son entrée ModelState. (Cela pourrait facilement être étendu pour les autres méthodes d'extension d'assistance).

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

Appel d'un contrôleur comme celui-ci:

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

ou depuis une vue comme celle-ci:

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

Il utilise System.Web.Mvc.ExpressionHelperpour obtenir le nom de la propriété ModelState.

Tobias J
la source
1
Très agréable! Garder un onglet à ce sujet pour la fonctionnalité ExpressionHelper.
Gerard ONeill
4

Je voulais mettre à jour ou réinitialiser une valeur si elle ne validait pas tout à fait et j'ai rencontré ce problème.

La réponse simple, ModelState.Remove, est… problématique… parce que si vous utilisez des helpers, vous ne connaissez pas vraiment le nom (sauf si vous vous en tenez à la convention de dénomination). Sauf si vous créez une fonction que votre assistant personnalisé et votre contrôleur peuvent utiliser pour obtenir un nom.

Cette fonctionnalité aurait dû être implémentée en option sur l'assistant, où, par défaut, elle ne le fait pas , mais si vous vouliez que l'entrée non acceptée soit réaffiche, vous pouvez simplement le dire.

Mais au moins je comprends le problème maintenant;).

Gérard ONeill
la source
J'avais besoin de faire exactement cela; voir mes méthodes que j'ai postées ci-dessous qui m'ont aidé à trouver Remove()la bonne clé.
Tobias J
0

J'ai compris à la fin. Mon ModelBinder personnalisé qui n'était pas en cours d'enregistrement et fait ceci:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

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

Donc, quelque chose que faisait la liaison de modèle par défaut devait être à l'origine du problème. Je ne sais pas quoi, mais mon problème est au moins résolu maintenant que mon classeur de modèles personnalisés est en cours d'enregistrement.

Monsieur Grok
la source
Eh bien, je n'ai aucune expérience avec un ModelBinder personnalisé, celui par défaut correspond à mes besoins jusqu'à présent =).
JOBG
0

Généralement, lorsque vous vous retrouvez à lutter contre un cadre de pratiques standard, il est temps de reconsidérer votre approche. Dans ce cas, le comportement de ModelState. Par exemple, lorsque vous ne voulez pas l'état du modèle après un POST, envisagez une redirection vers le fichier get.

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

MODIFIÉ pour répondre au commentaire sur la culture:

Voici ce que j'utilise pour gérer une application MVC multiculturelle. Tout d'abord, les sous-classes du gestionnaire d'itinéraire:

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
}

Et voici comment je câbler les itinéraires. Après avoir créé les routes, j'ajoute mon sous-agent (example.com/subagent1, example.com/subagent2, etc.) puis le code de culture. Si vous n'avez besoin que de la culture, supprimez simplement le sous-agent des gestionnaires de routes et des routes.

    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
la source
Vous avez tout à fait raison de suggérer la pratique POST REDIRECT, en fait je le fais pour presque chaque action post. Cependant j'avais un besoin très particulier: j'ai un formulaire de filtre en haut de la page, initialement soumis avec get. Mais j'ai rencontré un problème avec un champ de date non lié, puis j'ai découvert que les demandes GET ne véhiculaient pas la culture (j'utilise le français pour mon application), j'ai donc dû passer la demande à POST pour lier avec succès ma date. Puis est venu ce problème, je suis un peu coincé.
Souhaieb Besbes
@SouhaiebBesbes Voir mes mises à jour montrant comment je gère la culture.
B2K
@SouhaiebBesbes serait peut-être un peu plus simple de stocker votre culture dans TempData. Voir stackoverflow.com/questions/12422930/…
B2K
0

Eh bien, cela semblait fonctionner sur ma page Razor et n'a même jamais fait un aller-retour vers le fichier .cs. C'est une vieille méthode html. Cela pourrait être utile.

<input type="reset" value="Reset">
JustJohn
la source