Asp.net MVC ModelState.Clear

116

Может ли кто-нибудь дать мне краткое определение роли ModelState в Asp.net MVC (или ссылку на один). В частности, мне нужно знать, в каких ситуациях нужно или желательно звонить ModelState.Clear().

Немного открытый, да ... извините, я думаю, это может помочь, если я расскажу вам, что я на самом деле делаю:

У меня есть действие редактирования на контроллере под названием «Страница». Когда я впервые вижу форму для изменения деталей страницы, все загружается нормально (привязка к объекту «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 01

Ответы:

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
Просто сделал почти то же самое, что и вы. Однако выяснилось, что это не ошибка. Это задумано: ошибка? EditorFor и DisplayFor не отображают одинаковое значение, а Html-помощники ASP.NET MVC
Metro Smurf,
8
Чувак, я уже 2 часа борюсь с этим. Спасибо, что разместили этот ответ!
Андрей Агибалов
37
это по-прежнему верно, и многие люди, включая меня, теряют из-за этого много времени. ошибка или по дизайну, мне все равно, это "неожиданно".
Proviste
7
Я согласен с @Proviste, я надеюсь, что эта "функция" будет удалена в будущем
Бен
8
Я потратил на это четыре часа. Некрасиво.
Брайан Маккей
46

Обновить:

  • Это не ошибка.
  • Пожалуйста, прекратите возвращаться View()из действия POST. Вместо этого используйте PRG и перенаправьте на GET, если действие выполнено успешно.
  • Если будут возвращая View()из действия в POST, сделайте это для проверки формы, и сделать это так , как MVC разработан с использованием встроенного в помощниках. Если вы сделаете это таким образом, вам не нужно использовать.Clear()
  • Если вы используете это действие для возврата ajax для SPA , используйте веб-контроллер api и забудьте об этом, ModelStateпоскольку вам все равно не следует его использовать.

Старый ответ:

ModelState в MVC используется в первую очередь для описания состояния объекта модели в значительной степени в зависимости от того, является ли этот объект действительным или нет. Этот урок должен многое объяснить.

Как правило, вам не нужно очищать ModelState, поскольку он поддерживается механизмом MVC для вас. Очистка вручную может привести к нежелательным результатам при попытке придерживаться передовых методов проверки MVC.

Похоже, вы пытаетесь установить для заголовка значение по умолчанию. Это должно быть сделано, когда объект модели создается (уровень домена где-то или в самом объекте - ctor без параметров), в действии get, чтобы он спускался на страницу в первый раз или полностью на клиенте (через ajax или что-то еще) так что он выглядит так, как если бы пользователь ввел его, и он возвращается с коллекцией опубликованных форм. Некоторым образом ваш подход к добавлению этого значения при получении коллекции форм (в действии POST // Edit) вызывает такое странное поведение, которое может привести к .Clear() появлению работающего на вас. Поверьте мне - вы не хотите использовать клир. Попробуйте одну из других идей.

Мэтт Кодж
источник
1
Помогает мне немного переосмыслить уровень моих сервисов (стон, но спасибо), но, как и в случае с множеством вещей в сети, он сильно склоняется к точке зрения использования ModelState для проверки.
Mr Grok
К вопросу добавлена ​​дополнительная информация, чтобы показать, почему меня особенно интересует ModelState.Clear () и причина моего запроса
г-н Грок,
5
Я действительно не верю этому аргументу, чтобы перестать возвращать View (...) из функции [HttpPost]. Если вы отправляете содержимое POST через ajax, а затем обновляете документ полученным PartialView, было показано, что MVC ModelState неверен. Единственный обходной путь, который я нашел, - это очистить его в методе контроллера.
Аарон Худон
@AaronHudon PRG довольно хорошо зарекомендовал себя.
Мэтт Кодж
Если я выполняю POST с вызовом AJAX, могу ли я перенаправить на действие GET и вернуть представление, заполненное моделью, как того хочет OP, все асинхронно?
MyiEye 03
17

Если вы хотите очистить значение для отдельного поля, я нашел полезным следующий прием.

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

Примечание. Замените «Ключ» на имя поля, которое вы хотите сбросить.

Карл Сондерс
источник
Я не знаю, почему у меня это сработало по-другому (возможно, MVC4)? Но потом мне пришлось сделать model.Key = "". Обе строки обязательны.
TTT
Хочу поздравить вас с удаленным комментарием @PeterGluck. Это лучше, чем очищать полное состояние модели (поскольку у меня есть ошибки в некоторых полях, которые я хотел бы сохранить).
Tjab
6

Ну, ModelState в основном содержит текущее состояние модели с точки зрения проверки,

ModelErrorCollection: представляют ошибки, когда модель пытается привязать значения. ех.

TryUpdateModel();
UpdateModel();

или как параметр в ActionResult

public ActionResult Create(Person person)

ValueProviderResult : сохраните сведения о попытке привязки к модели. ех. AttemptedValue, Культура, RawValue .

Метод Clear () следует использовать с осторожностью, поскольку он может привести к неожиданным результатам. И вы потеряете некоторые приятные свойства ModelState, такие как AttemptedValue, это используется MVC в фоновом режиме для повторного заполнения значений формы в случае ошибки.

ModelState["a"].Value.AttemptedValue
JOBG
источник
1
хммм ... Судя по всему, здесь у меня проблема. Я проверил значение свойства Model.SeoTitle, и оно изменилось, но попытка значения не изменилась. Похоже, что он вставляет значение, как будто на странице есть ошибка, хотя ее нет (проверил словарь ModelState, ошибок нет).
Mr Grok
6

У меня был случай, когда я хотел обновить модель принятой формы и не хотел «перенаправлять на действие» по причине производительности. Предыдущие значения скрытых полей сохранялись в моей обновленной модели, что приводило к множеству проблем !.

Несколько строк кода вскоре идентифицировали элементы в ModelState, которые я хотел удалить (после проверки), поэтому новые значения были использованы в форме: -

while (ModelState.FirstOrDefault(ms => ms.Key.ToString().StartsWith("SearchResult")).Value != null)
{
    ModelState.Remove(ModelState.FirstOrDefault(ms => ms.Key.ToString().StartsWith("SearchResult")));
}
stevieg
источник
5

Что ж, похоже, многих из нас это укусило, и хотя причина, по которой это происходит, имеет смысл, мне нужен был способ убедиться, что значение моей модели было показано, а не 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()));
            }
        }

    }
B2K
источник
Вы совершенно правы, предлагая практику POST REDIRECT, на самом деле я делаю это почти для каждого поста. Однако у меня была очень конкретная потребность: у меня есть форма фильтра вверху страницы, изначально она была отправлена ​​с помощью get. Но я столкнулся с проблемой, когда поле даты не было привязано, а затем обнаружил, что запросы GET не передают культуру (я использую французский язык для своего приложения), поэтому мне пришлось переключить запрос на POST, чтобы успешно привязать мою дату. Потом возникла эта проблема, я ее немного застрял ..
Souhaieb Besbes
@SouhaiebBesbes Смотрите мои обновления, показывающие, как я отношусь к культуре.
B2K
@SouhaiebBesbes, возможно, проще было бы сохранить вашу культуру в TempData. См. Stackoverflow.com/questions/12422930/…
B2K
0

Что ж, похоже, это сработало на моей странице Razor, и даже не выполняло обхода к файлу .cs. Это старый способ HTML. Может быть полезно.

<input type="reset" value="Reset">
JustJohn
источник