Подтвердить что ты не робот

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

Кажется, у меня проблема с ASP.NET MVC в том, что, если у меня есть несколько форм на странице, которая использует одно и то же имя в каждом из них, но как разные типы (радио/скрытые/etc), тогда, когда первые записи формы (например, я выбираю "Дату" ), если форма повторно отображается (например, как часть страницы результатов), у меня, похоже, проблема, что скрытое значение 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.Hidden(...)% > ),.NET MVC заменяет их каждый раз.

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

Update:

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

4b9b3361

Ответ 1

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

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

Решение 1

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

Решение 2

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

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

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

Ответ 2

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

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

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

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

  • ModelState.Clear(): очищает все значения ModelState, но в основном отключает использование проверки по умолчанию в MVC
  • ModelState.Remove( "SomeKey" ): То же, что ModelState.Clear(), но требует микроуправления ключами ModelState, что слишком много работает, и это не кажется правильным с функцией автоматической привязки от MVC. Чувствует себя как 20 лет назад, когда мы также управляем ключами формы и QueryString.
  • Рендеринг HTML файлов: слишком много работы, детализация и отбрасывает методы HTML-помощника с дополнительными функциями. Пример: Replace @Html.HiddenFor by 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 получает приоритет над значением модели.

Решение

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

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

Ответ 3

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

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

Ответ 4

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

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

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

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

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

  • ModelState
  • ViewData​​li >
  • Значение (передано в TextBox() вызывающим)

Ответ 5

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

Ответ 6

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

Ответ 7

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

    [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 ) %>

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

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

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

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

Ответ 8

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

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.InputExtensions

Сводка: возвращает скрытый тег ввода.

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

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

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

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

Или я что-то не хватает?

Ответ 9

Эта проблема все еще существует в 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));
        }

Ответ 10

Итак, в MVC 4 "проблема дизайна" все еще существует. Здесь код, который я должен был использовать, чтобы установить правильные скрытые значения в коллекции, так как независимо от того, что я делаю в контроллере, представление всегда показывало неправильные значения.

OLD-код

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?

Ответ 11

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

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

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

Update: Здесь удобное расширение 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)

Ответ 12

Существует обходное решение:

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