Возможная ошибка в ASP.NET MVC с заменой значений формы - PullRequest
40 голосов
/ 27 февраля 2009

У меня, похоже, проблема с ASP.NET MVC в том, что если у меня есть несколько форм на странице, которые используют одно и то же имя в каждой, но в качестве разных типов (радио / скрытый / и т. Д.), То когда первая форма отправляет сообщение (я выбираю радиокнопку «Дата», например), если форма перерисовывается (скажем, как часть страницы результатов), у меня возникает проблема, связанная со скрытым значением SearchType на остальные формы изменяются на последнее значение переключателя (в данном случае SearchType.Name).

Ниже приведен пример формы для сокращения.

<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
  <%= Html.RadioButton("SearchType", SearchType.Date, true) %>
  <%= Html.RadioButton("SearchType", SearchType.Name) %>
  <input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>

<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
  <%= Html.Hidden("SearchType", SearchType.Colour) %>
  <input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>

<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
  <%= Html.Hidden("SearchType", SearchType.Reference) %>
  <input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>

Результирующий источник страницы (это будет частью страницы результатов)

<form action="/Search/Search" method="post">
  <input type="radio" name="SearchType" value="Date" />
  <input type="radio" name="SearchType" value="Name" />
  <input type="submit" name="submitForm" value="Submit" />
</form>

<form action="/Search/Search" method="post">
  <input type="hidden" name="SearchType" value="Name" /> <!-- Should be Colour -->
  <input type="submit" name="submitForm" value="Submit" />
</form>

<form action="/Search/Search" method="post">
  <input type="hidden" name="SearchType" value="Name" /> <!-- Should be Reference -->
  <input type="submit" name="submitForm" value="Submit" />
</form>

Пожалуйста, кто-нибудь еще с RC1 может это подтвердить?

Может быть, это потому, что я использую перечисление. Я не знаю. Я должен добавить, что я могу обойти эту проблему, используя теги input () для скрытых полей вручную, но если я использую теги MVC (<% = Html.Hidden (...)%>), .NET MVC заменяет их каждый раз.

Большое спасибо.

Обновление:

Сегодня я снова видел эту ошибку. Кажется, что это обрезает голову, когда вы возвращаете опубликованную страницу и используете MVC для установки скрытых тегов формы с помощью помощника HTML. Я связался с Филом Хааком по этому поводу, потому что я не знаю, куда еще обратиться, и я не верю, что это ожидаемое поведение, как указано Дэвидом.

Ответы [ 12 ]

36 голосов
/ 06 марта 2009

Да, такое поведение в настоящее время предусмотрено. Даже если вы явно устанавливаете значения, если вы отправляете обратно по тому же URL, мы смотрим в состояние модели и используем значение там. В целом, это позволяет нам отображать значение, отправленное вами при обратной передаче, а не исходное значение.

Существует два возможных решения:

Раствор 1

Используйте уникальные имена для каждого из полей. Обратите внимание, что по умолчанию мы используем указанное вами имя в качестве идентификатора HTML-элемента. Недопустимый HTML, если несколько элементов имеют одинаковый идентификатор. Поэтому использование уникальных имен является хорошей практикой.

Раствор 2

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

<input type="hidden" name="the-name" 
  value="<%= Html.AttributeEncode(Model.Value) %>" />

Конечно, когда я больше об этом думаю, изменение значения на основе обратной передачи имеет смысл для текстовых полей, но менее важно для скрытых входных данных. Мы не можем изменить это для v1.0, но я рассмотрю это для v2. Но нам нужно тщательно продумать последствия такого изменения.

10 голосов
/ 08 июня 2015

Так же, как и другие, я ожидал, что ModelState будет использоваться для заполнения Модели, и поскольку мы явно используем Модель в выражениях в представлении, она должна использовать Model, а не ModelState.

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

Единственное, что я не понимаю, это: , почему не по замыслу используется Модель, которая явно указана разработчиком, и если произошла ошибка проверки, используется ModelState.

Я видел много людей, использующих обходные пути, такие как

  • ModelState.Clear (): очищает все значения ModelState, но в основном отключает использование проверки по умолчанию в MVC
  • ModelState.Remove ("SomeKey"): то же самое, что ModelState.Clear (), но требует микроуправления клавишами ModelState, что является слишком большой работой и не работает с функцией автоматической привязки из MVC. Кажется, 20 лет назад, когда мы также управляли ключами Form и QueryString.
  • Рендеринг самих HTML: слишком много работы, деталей и отбрасывает методы HTML Helper с дополнительными функциями. Пример: заменить @ Html.HiddenFor на m.Name) "id =" @ Html.IdFor (m => m.Name) "value =" @ Html.AttributeEncode (Model.Name) ">. Или заменить @Html. DropDownListFor by ...
  • Создание пользовательских помощников HTML для замены стандартных помощников HTML MVC во избежание проблем с дизайном. Это более общий подход, чем рендеринг вашего HTML, но все же требуется больше знаний HTML + MVC или декомпиляция System.Web.MVC, чтобы сохранить все остальные функции, но отключить приоритет ModelState над Model.
  • Применение шаблона POST-REDIRECT-GET: это легко в некоторых средах, но сложнее в тех, где больше взаимодействия / сложности. У этого шаблона есть свои плюсы и минусы, и вам не следует применять этот шаблон из-за индивидуального выбора ModelState вместо Model.

Выпуск

Таким образом, проблема в том, что Модель заполняется из ModelState и в представлении, которое мы явно настроили для использования Модели. Все ожидают, что будет использовано значение модели (если оно изменилось), если только нет ошибки проверки; тогда можно использовать ModelState.

В настоящее время в расширениях MVC Helper значение ModelState имеет приоритет над значением Model.

Решение

Таким образом, фактическое исправление для этой проблемы должно быть таким: для каждого выражения, получающего значение модели, значение ModelState должно быть удалено, если для этого значения нет ошибки проверки. Если для этого элемента управления вводится ошибка проверки, значение ModelState не следует удалять, и оно будет использоваться как обычно. Я думаю, что это решает проблему точно, что лучше, чем большинство обходных путей.

Код здесь:

    /// <summary>
    /// Removes the ModelState entry corresponding to the specified property on the model if no validation errors exist. 
    /// 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 HtmlHelper helper,  
        Expression<Func<TModel, TProperty>> expression)
    {
        //First get the expected name value. This is equivalent to helper.NameFor(expression)
        string name = ExpressionHelper.GetExpressionText(expression);
        string fullHtmlFieldName = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);

        //Now check whether modelstate errors exist for this input control
        ModelState modelState;
        if (!helper.ViewData.ModelState.TryGetValue(fullHtmlFieldName, out modelState) ||
            modelState.Errors.Count == 0)
        {
            //Only remove ModelState value if no modelstate error exists,
            //so the ModelState will not be used over the Model
            helper.ViewData.ModelState.Remove(name);
        }
    }

И затем мы создаем наши собственные расширения HTML Helper, чтобы сделать это перед вызовом расширений MVC:

    public static MvcHtmlString TextBoxForModel<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression,
        string format = "",
        Dictionary<string, object> htmlAttributes = null)
    {
        RemoveStateFor(htmlHelper, expression);
        return htmlHelper.TextBoxFor(expression, format, htmlAttributes);
    }

    public static IHtmlString HiddenForModel<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression)
    {
        RemoveStateFor(htmlHelper, expression);
        return htmlHelper.HiddenFor(expression);
    }

Это решение устраняет проблему, но не требует, чтобы вы декомпилировали, анализировали и перестраивали то, что MVC обычно предлагает вам (не забывайте также управлять изменениями во времени, различиями браузера и т. Д.).

Я думаю, что логика "Значение модели, если только ошибка проверки, то ModelState" должна была быть задуманна. Если бы это было так, это не укусило бы так много людей, но все равно охватывало бы то, что MVC должен был сделать.

6 голосов
/ 13 октября 2011

Heads-up - эта ошибка все еще существует в MVC 3. Я использую синтаксис разметки Razor (как это действительно имеет значение), но я столкнулся с той же ошибкой с циклом foreach, который выдает одно и то же значение для свойства объекта каждый разовое.

6 голосов
/ 03 октября 2009

Я только что столкнулся с той же проблемой. Html-помощники, такие как TextBox () для переданных значений, ведут себя совершенно противоположно тому, что я понял из Документации , где написано:

Значение элемента ввода текста. Если это значение является нулевой ссылкой (Ничего в Visual Basic), значение элемента извлекается из объект ViewDataDictionary. Если значения там не существует, значение извлекается из объекта ModelStateDictionary.

Я прочитал, что используется значение, если оно передано. Но читая источник TextBox ():

string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);

, кажется, указывает на то, что фактический порядок является полной противоположностью того, что задокументировано Фактический порядок выглядит следующим образом:

  1. ModelState
  2. ViewData
  3. Значение (передается в TextBox () вызывающим абонентом)
5 голосов
/ 27 февраля 2009

Это было бы ожидаемое поведение - MVC не использует состояние представления или другое за вашими уловками для передачи дополнительной информации в форме, поэтому он не знает, какую форму вы отправили (имя формы не является частью данных отправлено, только список пар имя / значение).

Когда MVC отображает форму обратно, он просто проверяет, существует ли переданное значение с тем же именем - опять же, у него нет возможности узнать, из какой формы пришло именованное значение, или даже какой тип элемента управления был (используете ли вы радио, текст или скрыто, все это просто имя = значение при отправке через HTTP).

4 голосов
/ 15 февраля 2011
foreach (var s in ModelState.Keys.ToList())
                if (s.StartsWith("detalleProductos"))
                    ModelState.Remove(s);

ModelState.Remove("TimeStamp");
ModelState.Remove("OtherOfendingHiddenFieldNamePostedToSamePage1");
ModelState.Remove("OtherOfendingHiddenFieldNamePostedToSamePage2");

return View(model);
3 голосов
/ 18 февраля 2014

Эта проблема все еще существует в MVC 5, и, очевидно, она не считается ошибкой, что нормально.

Мы обнаруживаем, что, хотя по замыслу, это не ожидаемое поведение для нас. Скорее, мы всегда хотим, чтобы значение скрытого поля действовало аналогично другим типам полей и не обрабатывалось специальным образом, или извлекало его значение из некоторой неясной коллекции (которая напоминает нам ViewState!).

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

  • Html.DisplayFor() отображает правильное значение (из модели)
  • Html.ValueFor нет (он тянет из ModelState)
  • ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model выводит правильное значение

Наше решение состоит в том, чтобы просто реализовать наше собственное расширение:

        /// <summary>
        /// Custom HiddenFor that addresses the issues noted here:
        /// /436779/vozmozhnaya-oshibka-v-asp-net-mvc-s-zamenoi-znachenii-formy
        /// We will only ever want values pulled from the model passed to the page instead of 
        /// pulling from modelstate.  
        /// Note, do not use 'ValueFor' in this method for these reasons.
        /// </summary>
        public static IHtmlString HiddenTheWayWeWantItFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                    Expression<Func<TModel, TProperty>> expression,
                                                    object value = null,
                                                    bool withValidation = false)
        {
            if (value == null)
            {
                value = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model;
            }

            return new HtmlString(String.Format("<input type='hidden' id='{0}' name='{1}' value='{2}' />",
                                    htmlHelper.IdFor(expression),
                                    htmlHelper.NameFor(expression),
                                    value));
        }
3 голосов
/ 23 марта 2010

Пример для воспроизведения «проблемы дизайна» и возможного обходного решения. Для 3-х часов потерянных попыток найти «ошибку» не существует ... Обратите внимание, что этот «дизайн» все еще присутствует в ASP.NET MVC 2.0 RTM.

    [HttpPost]
    public ActionResult ProductEditSave(ProductModel product)
    {
        //Change product name from what was submitted by the form
        product.Name += " (user set)";

        //MVC Helpers are using, to find the value to render, these dictionnaries in this order: 
        //1) ModelState 2) ViewData 3) Value
        //This means MVC won't render values modified by this code, but the original values posted to this controller.
        //Here we simply don't want to render ModelState values.
        ModelState.Clear(); //Possible workaround which works. You loose binding errors information though...  => Instead you could replace HtmlHelpers by HTML input for the specific inputs you are modifying in this method.
        return View("ProductEditForm", product);
    }

Если ваша форма изначально содержит это: <%= Html.HiddenFor( m => m.ProductId ) %>

Если исходное значение «Имя» (когда форма была отображена) является «фиктивным», после отправки формы вы ожидаете увидеть «фиктивный (пользовательский набор)». Без ModelState.Clear() вы все равно увидите "пустышку" !!!!!!

Правильный обходной путь:

<input type="hidden" name="Name" value="<%= Html.AttributeEncode(Model.Name) %>" />

Я чувствую, что это не совсем хороший дизайн, так как каждый разработчик форм mvc должен помнить об этом.

1 голос
/ 21 декабря 2015

Есть обходной путь:

    public static class HtmlExtensions
    {
        private static readonly String hiddenFomat = @"<input id=""{0}"" type=""hidden"" value=""{1}"" name=""{2}"">";
        public static MvcHtmlString HiddenEx<T>(this HtmlHelper htmlHelper, string name, T[] values)
        {
            var builder = new StringBuilder(values.Length * 100);
            for (Int32 i = 0; i < values.Length; 
                builder.AppendFormat(hiddenFomat,
                                        htmlHelper.Id(name), 
                                        values[i++].ToString(), 
                                        htmlHelper.Name(name)));
            return MvcHtmlString.Create(builder.ToString());
        }
    }
1 голос
/ 12 декабря 2013

Так что в MVC 4 «проблема дизайна» все еще существует. Вот код, который мне пришлось использовать для установки правильных скрытых значений в коллекции, поскольку независимо от того, что я делаю в контроллере, представление всегда показывало неправильные значения.

СТАРЫЙ код

for (int i = 0; i < Model.MyCollection.Count; i++)
{
    @Html.HiddenFor(m => Model.MyCollection[i].Name) //It doesn't work. Ignores what I changed in the controller
}

ОБНОВЛЕННЫЙ код

for (int i = 0; i < Model.MyCollection.Count; i++)
{
    <input type="hidden" name="MyCollection[@(i)].Name" value="@Html.AttributeEncode(Model.MyCollection[i].Name)" /> // Takes the recent value changed in the controller!
}

Они исправили это в MVC 5?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...