Asp.net MVC ModelState.Clear

时间:2009-11-21 11:04:10

标签: asp.net-mvc modelstate post-redirect-get

任何人都可以给我一个关于ModelState在Asp.net MVC中的角色的简洁定义(或者链接到一个)。特别是我需要知道在什么情况下调用ModelState.Clear()是必要或可取的。

比特开放结束嗯 ...对不起,我想如果告诉你我在做什么可能会有所帮助:

我在控制器上有一个名为“Page”的Action。当我第一次看到表单来更改页面的详细信息时,所有内容都很好地加载(绑定到“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>

10 个答案:

答案 0 :(得分:132)

我认为是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); 
}

按预期工作。

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

答案 1 :(得分:38)

<强>更新

  • 这不是错误。
  • 请停止从POST操作返回View()。如果操作成功,请使用PRG并重定向到GET。
  • 如果 从POST操作中返回View(),请执行表单验证,并使用内置帮助程序MVC is designed的方式执行此操作。如果你这样做,那么你不需要使用.Clear()
  • 如果您使用此操作返回SPA的ajax,请使用web api控制器并忘记ModelState,因为您无论如何都不应该使用它。

旧回答:

MVC中的ModelState主要用于描述模型对象的状态,主要与该对象是否有效有关。 This tutorial应该解释很多。

通常,您不需要清除ModelState,因为它由MVC引擎为您维护。在尝试遵守MVC验证最佳实践时,手动清除它可能会导致意外结果。

您似乎正在尝试为标题设置默认值。这应该在模型对象被实例化时(在某个地方或在对象本身 - 无参数的ctor中),在get动作上进行,以便它第一次或完全在客户端(通过ajax或其他)下载到页面这样看起来好像用户输入了它,然后它返回了已发布的表单集合。一些如何在接收表单集合时添加此值的方法(在POST操作//编辑中)导致这种奇怪的行为可能导致.Clear() 出现您。相信我 - 你不想使用clear。尝试其他一个想法。

答案 2 :(得分:17)

如果要清除单个字段的值,我发现以下技术很有用。

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

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

答案 3 :(得分:6)

根据验证,ModelState基本上保持模型的当前状态,它保持

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

TryUpdateModel();
UpdateModel();

或类似ActionResult中的参数

public ActionResult Create(Person person)

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

Clear()方法必须谨慎使用,因为它可能导致意外结果。并且你将丢失ModelState的一些不错的属性,如AttemptedValue,MVC在后台使用它来重新填充表单值以防错误。

ModelState["a"].Value.AttemptedValue

答案 4 :(得分:6)

我有一个实例,我想更新一个总结形式的模型,并且不想因为性能原因而“重定向到行动”。隐藏字段的先前值保留在我更新的模型上 - 导致各种问题!。

几行代码很快就确定了我想删除的ModelState中的元素(在验证之后),因此新值在表单中使用: -

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

答案 5 :(得分:5)

我们很多人似乎都对此感到厌烦,虽然这种情况发生的原因是有道理的,我需要一种方法来确保我的模型上的值显示出来,而不是ModelState。

有些人建议ModelState.Remove(string key),但key应该是什么并不明显,特别是对于嵌套模型。以下是我提出的一些方法来帮助解决这个问题。

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

/// <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属性的名称。

答案 6 :(得分:4)

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

简单的答案,ModelState.Remove,有问题..因为如果你使用帮助器,你真的不知道名称(除非你遵守命名约定)。除非你创建一个函数,你的自定义助手和你的控制器都可以使用它来获取名称。

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

但至少我现在理解这个问题;)。

答案 7 :(得分:0)

最后得到它。我的自定义ModelBinder未注册并执行此操作:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

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

默认模型绑定所做的事情一直是导致问题的原因。不确定是什么,但我的问题至少已经修复,因为我的自定义模型绑定器正在注册。

答案 8 :(得分: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()));
            }
        }

    }

答案 9 :(得分:0)

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

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