Asp.net MVC ModelState.Clear


116

谁能给我关于Asp.net MVC中ModelState角色的简要定义(或指向其中的链接)。特别是我需要知道在什么情况下有必要或希望致电ModelState.Clear()

有点开放了吧 ...抱歉,如果告诉您我在做什么,可能会有所帮助:

我在名为“页面”的控制器上进行了“编辑”操作。当我第一次看到用于更改Page详细信息的表单时,所有内容都会正常加载(绑定到“ MyCmsPage”对象)。然后,我单击一个为MyCmsPage对象的字段之一生成值的按钮(MyCmsPage.SeoTitle)。它会很好地生成并更新对象,然后我将返回带有新修改的页面对象的操作结果,并期望相关的文本框(使用渲染<%= Html.TextBox("seoTitle", page.SeoTitle)%>)会被更新……但是,它会显示已加载的旧模型中的值。

我已经通过使用来解决它,ModelState.Clear()但是我需要知道它为什么/如何工作,所以我不仅仅是盲目地做。

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>

Noob AspMVC,如果它想缓存旧数据,那么再次为用户提供模型有什么意义:@我也遇到了同样的问题,非常感谢兄弟
deadManN 2015年

Answers:


135

我认为这是MVC中的错误。我今天在这个问题上挣扎了几个小时。

鉴于这种:

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

视图将使用原始模型进行渲染,而忽略更改。所以我想,也许它不喜欢我使用相同的模型,所以我尝试这样:

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

而且视图仍然使用原始模型进行渲染。奇怪的是,当我在视图中放置一个断点并检查模型时,它具有更改后的值。但是响应流具有旧值。

最终,我发现与您所做的工作相同:

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

可以正常工作。

我不认为这是“功能”,对吗?


33
只是做了几乎与您完全相同的事情。但是发现这不是错误。这是设计使然Bug?EditorFor和DisplayFor不会显示相同的值,并且ASP.NET MVC的HTML帮助器呈现了错误的值
Metro Smurf

8
伙计,我已经花了2个小时与之抗争。感谢您发布此答案!
Andrey Agibalov 2012年

37
事实仍然如此,因此包括我在内的许多人正在浪费很多时间。错误或设计使然,我不在乎,这是“意外的”。
2012年

7
我同意@Proviste,我希望这个“功能”在未来被删除

8
我刚刚花了四个小时。丑陋。
Brian MacKay 2013年

46

更新:

  • 这不是错误。
  • 请停止View()从POST操作返回。如果操作成功,请改用PRG并重定向到GET。
  • 如果您返回View()从POST动作,连续做表单验证,并做到这一点的方式MVC设计使用内置的助手。如果您以这种方式进行操作,则无需使用.Clear()
  • 如果您正在使用此操作返回SPA的 Ajax ,请使用Web api控制器,然后忘记了,ModelState因为您还是不应该使用它。

旧答案:

MVC中的ModelState主要用于描述模型对象的状态,很大程度上取决于该对象是否有效。本教程应该解释很多。

通常,您不需要清除ModelState,因为它由MVC引擎为您维护。尝试遵循MVC验证的最佳做法时,手动清除它可能会导致不良结果。

似乎您正在尝试为标题设置默认值。当实例化模型对象时(在对象本身或对象层中的域层-无参数ctor),应在get动作上实例化该对象,以便它第一次或完全在客户端上(通过ajax或其他方式)下降到页面这样就好像用户输入了它,并且它与已发布的表单集合一起返回。您在接收表单集合时(在POST操作中//编辑)添加此值的方法是如何导致这种奇怪的行为的,可能会导致.Clear() 看起来适合您。相信我-您不想使用透明纸。尝试其他想法之一。


1
确实帮助我重新考虑了服务层(虽然gro琐,但很麻烦),但是由于网上有很多东西,它严重倾向于使用ModelState进行验证。
Grok先生

向问题中添加了更多信息,以显示为什么我对ModelState.Clear()特别感兴趣,以及进行查询的原因
Grok先生,

5
我真的不买这个参数来停止从[HttpPost]函数返回View(...)。如果通过ajax发布内容,然后使用生成的PartialView更新文档,则表明MVC ModelState不正确。我发现的唯一解决方法是在controller方法中将其清除。
亚伦·休顿

@AaronHudon PRG已经非常完善。
马特·科卡伊

如果我通过AJAX调用进行POST,是否可以异步重定向到GET操作并返回OP想要的模型填充视图?
MyiEye

17

如果您想清除单个字段的值,那么我发现以下技巧很有用。

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

注意:将 “密钥”更改为要重置的字段的名称。


我不知道为什么这对我有不同的作用(也许是MVC4)?但是之后我还必须做model.Key =“”。两行都是必需的。
TTT

我想赞扬您@PeterGluck的评论。这比清除完整的模型状态更好(因为在某些我想保留的字段上有错误)。
Tjab '16

6

那么ModelState基本上就验证而言拥有模型的当前状态,

ModelErrorCollection:表示模型尝试绑定值时的错误。例如

TryUpdateModel();
UpdateModel();

或类似于ActionResult中的参数

public ActionResult Create(Person person)

ValueProviderResult:保存有关尝试绑定到模型的详细信息。例如 AttemptedValue,Culture,RawValue

必须谨慎使用Clear()方法,因为它可能导致未指定的结果。而且您会丢失ModelState的一些不错的属性,例如AttemptedValue,MVC在后台使用它来在出现错误的情况下重新填充表单值。

ModelState["a"].Value.AttemptedValue

1
嗯...从外观上看,这可能就是我要解决的问题。我已经检查了Model.SeoTitle属性的值,它已更改,但尝试的值未更改。即使没有错误(好像检查了ModelState词典,也没有错误),看起来好像在粘贴值,好像页面上有错误。
格罗克先生

6

我有一个实例,其中我想更新报价表单的模型,并且出于性能考虑,不想“重定向到操作”。隐藏字段的先前值保留在我的更新模型中-引起各种问题!

几行代码很快就确定了ModelState中要删除的元素(在验证之后),因此新值以以下形式使用:-

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

5

我们中的很多人似乎都为此感到痛苦,尽管发生这种情况的原因很合理,但我需要一种方法来确保显示Model的值,而不是ModelState的值。

有人提出了建议ModelState.Remove(string key),但是key应该做什么并不清楚,特别是对于嵌套模型。这是我想出的几种方法来协助解决此问题。

RemoveStateFor方法将为ModelStateDictionary所需的属性采用,模型和表达式,然后将其删除。HiddenForModel可以在视图中使用它,通过首先删除其ModelState条目,仅使用模型中的值来创建隐藏的输入字段。(可以很容易地将其扩展为其他辅助扩展方法)。

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

从这样的控制器中调用:

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

或从这样的观点来看:

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

它用于System.Web.Mvc.ExpressionHelper获取ModelState属性的名称。


1
非常好!在此保留一个选项卡以使用ExpressionHelper功能。
杰拉德·奥尼尔

4

我想更新或重置一个值,如果它没有完全验证,并遇到了这个问题。

答案很简单,就是ModelState.Remove,这是有问题的,因为如果使用助手,您实际上并不知道名称(除非您遵循命名约定)。除非您可能创建了一个函数,您的自定义助手和控制器都可以使用该函数来获取名称。

该功能应该已经作为帮助程序上的一个选项实现,默认情况下不会执行此功能,但是如果您希望重新显示不可接受的输入,则可以这样说。

但是至少我现在明白了这个问题;)。


我需要做到这一点。请参阅下面发布的方法,这些方法可以帮助我Remove()正确设置密钥。
Tobias J

0

最后得到它。我尚未注册的Custom ModelBinder并执行以下操作:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

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

因此,默认模型绑定正在执行的操作一定是导致此问题的原因。不知道是什么,但是我的问题至少已经解决,因为我的自定义模型活页夹正在注册。


好吧,我没有使用自定义ModelBinder的经验,到目前为止,默认值适合我的需求=)。
JOBG

0

通常,当您发现自己与框架标准实践作斗争时,该重新考虑您的方法了。在这种情况下,为ModelState的行为。例如,当您不希望在POST之后获得模型状态时,可以考虑重定向到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);
}

编辑以回答文化评论:

这是我用来处理多文化MVC应用程序的内容。首先,路由处理程序子类:

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
}

这是我连接路线的方式。创建路由后,我先添加子代理(example.com/subagent1、example.com/subagent2等),然后添加区域性代码。如果您只需要区域性,则只需从路由处理程序和路由中删除子代理即可。

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

    }

您非常正确地建议POST REDIRECT的做法,实际上我几乎对每个post操作都这样做。但是我有一个非常特殊的需要:我在页面顶部有一个过滤器表单,最初是使用get提交的。但是我遇到了一个日期字段未绑定的问题,然后发现GET请求没有沿用这种文化(我在我的应用程序中使用法语),因此我不得不将请求切换到POST才能成功绑定我的日期。然后来到这个问题,我有点卡住她的..
Souhaieb Besbes

@SouhaiebBesbes查看我的更新,以显示我如何处理文化。
B2K

@SouhaiebBesbes可能会更简单一些,将您的文化存储在TempData中。请参阅stackoverflow.com/questions/12422930/…–
B2K

0

好吧,这似乎可以在我的Razor页面上使用,甚至从未往返于.cs文件。这是旧的html方式。这可能很有用。

<input type="reset" value="Reset">
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.