Возможная ошибка в ASP.NET MVC с заменой значений формы

StackOverflow https://stackoverflow.com/questions/594600

Вопрос

Похоже, у меня возникли проблемы с ASP.NET MVC в том, что, если у меня есть более одной формы на странице, которая использует одно и то же имя в каждой из них, но в виде разных типов (radio / hidden / etc), То, когда публикуется первая форма (например, я выбираю переключатель 'Date'), если форма повторно отображается (скажем, как часть страницы результатов), у меня, похоже, возникает проблема с тем, что скрытое значение 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 подтвердить это?

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

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

Обновить:

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

Это было полезно?

Решение

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

Есть два возможных решения:

Решение 1

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

Решение 2

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

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

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

Другие советы

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

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

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

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

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

Проблема

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

В настоящее время в вспомогательных расширениях MVC значение 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 для этого перед вызовом расширений 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.

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

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

Что касается меня, я читал, что используется значение, если оно передано.Но чтение текстового поля() источник:

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

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

  1. Состояние модели
  2. Просмотр данных
  3. Значение (передается в текстовое поле() вызывающим объектом)

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

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

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

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 часов, потерянных на попытки найти "ошибку" ...Обратите внимание, что этот "дизайн" все еще находится в версии 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 ) %>

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

Правильное обходное решение:

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

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

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

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

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

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

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

        /// <summary>
        /// Custom HiddenFor that addresses the issues noted here:
        /// http://stackoverflow.com/questions/594600/possible-bug-in-asp-net-mvc-with-form-values-being-replaced
        /// 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));
        }

Это может быть "по замыслу", но это не то, что задокументировано:

Public Shared Function Hidden(  

  ByVal htmlHelper As System.Web.Mvc.HtmlHelper,  
  ByVal name As String, ByVal value As Object)  
As String  

Участник System.Web.Mvc.Html.Входные расширения

Краткие сведения:Возвращает скрытый тег ввода.

Параметры:
htmlHelper:HTML-помощник.
Имя:Имя поля формы и ключ System.Web.Mvc.ViewDataDictionary, используемые для поиска значения.
значение:Значение скрытого ввода.Если значение равно null, ищет значение в System.Web.Mvc.ViewDataDictionary, а затем в System.Web.Mvc.ModelStateDictionary.

Это, по-видимому, предполагает, что ТОЛЬКО тогда, когда параметр value равен null (или не указан), HtmlHelper будет искать значение в другом месте.

В моем приложении у меня есть форма, где:HTML.Скрытый ("удаленный", True) отображается как <input id="remote" name="remote" type="hidden" value="False" />

Обратите внимание, что значение переопределяется тем, что находится в ViewData.Словарь ModelState.

Или я что-то упускаю?

Таким образом, в 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?

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

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

Как предлагали другие, я выбрал использование прямого html-кода вместо использования HtmlHelpers (TextBoxFor, CheckBoxFor, HiddenFor и т.д.).

Однако проблема при таком подходе заключается в том, что вам нужно поместить атрибуты name и id в виде строк.Я хотел сохранить свойства моей модели строго типизированными, поэтому я использовал NameFor и IdFor HtmlHelpers.

<input type="hidden" name="@Html.NameFor(m => m.Name)" id="@Html.IdFor(m=>m.Name)" value="@Html.AttributeEncode(Model.Name)">

Обновить: Вот удобное расширение HtmlHelper

    public static MvcHtmlString MyHiddenFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, object htmlAttributes = null)
    {
        return new MvcHtmlString(
            string.Format(
                @"<input id=""{0}"" type=""hidden"" value=""{1}"" name=""{2}"">",
                helper.IdFor(expression),
                helper.NameFor(expression),
                GetValueFor(helper, expression)
            ));
    }

    /// <summary>
    /// Retrieves value from expression
    /// </summary>
    private static string GetValueFor<TModel, TValue>(HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression)
    {
        object obj = expression.Compile().Invoke(helper.ViewData.Model);
        string val = string.Empty;
        if (obj != null)
            val = obj.ToString();
        return val;
    }

Затем вы можете использовать его как

@Html.MyHiddenFor(m => m.Name)
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top