Quelqu'un peut-il me donner une définition succincte du rôle de ModelState dans Asp.net MVC (ou un lien vers un lien)? En particulier, j'ai besoin de savoir dans quelles situations il est nécessaire ou souhaitable d'appeler ModelState.Clear()
.
Bit open ending huh ... désolé, je pense que cela pourrait aider si je vous disais 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 changer les détails de la page, tout se charge bien (liaison avec 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 retourne le résultat de l'action avec le nouvel objet de page 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, il affiche la valeur de la ancien modèle qui a été chargé.
J'ai travaillé autour de cela en utilisant ModelState.Clear()
mais j'ai besoin de savoir pourquoi/comment cela a fonctionné afin que je ne le fasse pas juste 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>
Je pense que c'est un bug 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 s'affiche avec le modèle d'origine, en ignorant les modifications. Alors j'ai pensé, peut-être que ça ne me plaît 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 restituée avec le modèle original. 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 changé de valeur. 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?
Mise à jour:
View()
à partir d'une action POST. Utilisez plutôt PRG et redirigez-le vers un élément GET si l'action est un succès.View()
à partir d'une action POST, faites-le pour la validation du formulaire et procédez comme suit MVC est conç en utilisant les helpers intégrés. Si vous le faites de cette façon, vous n’auriez pas besoin d’utiliser .Clear()
ModelState
car vous ne devriez pas le faire. 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 en fonction de sa validité ou non. Ce tutoriel devrait en expliquer beaucoup.
Généralement, vous ne devriez pas avoir besoin d'effacer le ModelState car il est géré par le moteur MVC pour vous. Si vous l'effacez manuellement, vous risquez d'obtenir des résultats indésirables lorsque vous essayez de vous conformer aux meilleures pratiques de validation de 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 descend à la page la première fois ou complètement sur le client (via ajax ou quelque chose). de sorte qu'il semble que l'utilisateur l'ait entré et qu'il revienne avec la collection de formulaires postés. Certains expliquent comment votre approche consistant à ajouter cette valeur à la réception d’une collection de formulaires (dans le POST action // Edit) provoque ce comportement étrange qui pourrait entraîner un .Clear()
apparaissant travailler pour vous. Croyez-moi, vous ne voulez pas utiliser le clair. Essayez l'une des autres idées.
Si vous souhaitez effacer une valeur pour un champ individuel, la technique suivante est utile.
ModelState.SetModelValue("Key", new ValueProviderResult(null, string.Empty, CultureInfo.InvariantCulture));
Remarque: Remplacez "Key" par le nom du champ que vous souhaitez réinitialiser.
J'ai eu un cas où je voulais mettre à jour le modèle d'un formulaire envoyé et que je ne voulais pas "Rediriger vers une action" pour des raisons de performance. Les valeurs précédentes de champs cachés étaient conservées sur mon modèle mis à jour, ce qui provoquait toutes sortes de problèmes !.
Quelques lignes de code ont rapidement identifié les éléments de ModelState que je voulais supprimer (après validation). Les nouvelles valeurs ont donc é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")));
}
Eh bien, le ModelState détient fondamentalement l’état actuel du modèle en termes de validation, il détient
ModelErrorCollection: Représente les erreurs lorsque le modèle tente de lier les valeurs. ex.
TryUpdateModel();
UpdateModel();
ou comme un paramètre dans ActionResult
public ActionResult Create(Person person)
ValueProviderResult: Conserve 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 entraîner des résultats inattendus. Et vous perdrez certaines propriétés Nice de ModelState telles que AttemptedValue. Ceci est utilisé par MVC en arrière-plan pour repeupler les valeurs de formulaire en cas d'erreur.
ModelState["a"].Value.AttemptedValue
Beaucoup d’entre nous semblent avoir été mordus par cela, et bien que la raison en soit logique, j’avais besoin d’un moyen de s’assurer que la valeur de mon modèle était affichée, et non de ModelState.
Certains ont suggéré ModelState.Remove(string key)
, mais ce que key
devrait être, n'est pas évident, en particulier pour les modèles imbriqués. Voici quelques méthodes que j'ai proposées pour vous aider.
La méthode RemoveStateFor
prendra un ModelStateDictionary
, un modèle et une expression pour la propriété souhaitée, puis le supprimera. HiddenForModel
peut être utilisé dans votre vue pour créer un champ de saisie 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 d'une vue comme celle-ci:
@Html.HiddenForModel(m => m.MySubProperty.MySubValue)
Il utilise System.Web.Mvc.ExpressionHelper
pour obtenir le nom de la propriété ModelState.
Je voulais mettre à jour ou réinitialiser une valeur si elle n’avait pas été validée, et je me suis heurté à ce problème.
La réponse facile, ModelState.Remove, est .. problématique .. car si vous utilisez des aides, vous ne connaissez pas vraiment le nom (sauf si vous respectez la convention de dénomination). À moins que vous ne créiez peut-être une fonction que votre aide custom et votre contrôleur peuvent utiliser pour obtenir un nom.
Cette fonctionnalité aurait dû être implémentée en tant qu'option sur l'assistant, où par défaut est pas, mais si vous voulez que l'entrée non acceptée réaffiche, vous pouvez simplement le dire.
Mais au moins, je comprends le problème maintenant;).
En règle générale, lorsque vous vous retrouvez en train de vous battre contre un cadre standard, il est temps de reconsidérer votre approche. Dans ce cas, le comportement de ModelState. Par exemple, lorsque vous ne souhaitez pas que l'état du modèle se termine après un POST, envisagez une redirection vers 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 de culture:
Voici ce que j'utilise pour gérer une application MVC multiculturelle. 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âble les itinéraires. Après avoir créé les routes, je commence par ajouter mon sous-agent (exemple.com/subagent1, exemple.com/subagent2, etc.), puis le code de culture. Si tout ce dont vous avez besoin, c'est de la culture, supprimez simplement le sous-agent des gestionnaires et des itinéraires.
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()));
}
}
}
Je l'ai eu à la fin. Mon ModelBinder personnalisé qui n'était pas enregistré et le fait:
var mymsPage = new MyCmsPage();
NameValueCollection frm = controllerContext.HttpContext.Request.Form;
myCmsPage.SeoTitle = (!String.IsNullOrEmpty(frm["seoTitle"])) ? frm["seoTitle"] : null;
Donc, quelque chose que la liaison de modèle par défaut faisait 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é est en cours d'enregistrement.
Eh bien, cela a semblé fonctionner sur ma page Razor et n’a même jamais fait un aller-retour vers le fichier .cs. C'est vieux moyen html. Cela pourrait être utile.
<input type="reset" value="Reset">