Возможная ошибка в ASP.NET MVC с заменой значений формы
Похоже, у меня проблема с 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 ответов
Да, это поведение в настоящее время разработано. Даже если вы явно устанавливаете значения, если вы отправляете обратно по тому же URL, мы смотрим в состояние модели и используем значение там. В целом, это позволяет нам отображать значение, отправленное вами при обратной передаче, а не исходное значение.
Есть два возможных решения:
Решение 1
Используйте уникальные имена для каждого из полей. Обратите внимание, что по умолчанию мы используем указанное вами имя в качестве идентификатора HTML-элемента. Недопустимый HTML, потому что несколько элементов имеют одинаковый идентификатор. Поэтому использование уникальных имен - это хорошая практика.
Решение 2
Не используйте скрытый помощник. Кажется, тебе это действительно не нужно. Вместо этого вы можете сделать это:
<input type="hidden" name="the-name"
value="<%= Html.AttributeEncode(Model.Value) %>" />
Конечно, когда я думаю об этом больше, изменение значения на основе обратной передачи имеет смысл для текстовых полей, но менее важно для скрытых входных данных. Мы не можем изменить это для v1.0, но я рассмотрю это для v2. Но нам нужно тщательно продумать последствия таких изменений.
Как и другие, я ожидал, что 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.
Решение
Таким образом, фактическое исправление для этой проблемы должно быть таким: для каждого выражения, получающего значение 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 должен был сделать.
Я просто столкнулся с той же проблемой. 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
- Значение (передается в TextBox() вызывающей стороной)
Heads-up - эта ошибка все еще существует в MVC 3. Я использую синтаксис разметки Razor (как это действительно имеет значение), но я столкнулся с той же ошибкой с циклом foreach, который каждый раз выдает одно и то же значение для свойства объекта.
Это было бы ожидаемое поведение - MVC не использует представление состояния или другое за вашими уловками для передачи дополнительной информации в форме, поэтому он не знает, какую форму вы отправили (имя формы не является частью представленных данных, только список пар имя / значение).
Когда MVC отображает форму обратно, он просто проверяет, существует ли отправленное значение с тем же именем, - опять же, у него нет возможности узнать, из какой формы пришло именованное значение, или даже какой тип элемента управления был (если вы используйте радио, текст или скрытый, все это просто имя = значение, когда оно отправлено через HTTP).
Эта проблема все еще существует в 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://stackru.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));
}
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 ) %>
Если исходное значение "Имя" (когда форма была отображена) - "пустышка", после отправки формы вы ожидаете увидеть "пустышку (пользовательский набор)". Без ModelState.Clear()
вы все равно увидите "пустышку"!!!!!!
Правильный обходной путь:
<input type="hidden" name="Name" value="<%= Html.AttributeEncode(Model.Name) %>" />
Я чувствую, что это не очень хороший дизайн, так как каждый разработчик mvc-форм должен помнить об этом.
Это может быть "разработано", но это не то, что задокументировано:
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, используемый для поиска значения.
значение: значение скрытого ввода. Если ноль, смотрит на значение System.Web.Mvc.ViewDataDictionary и затем System.Web.Mvc.ModelStateDictionary для значения.
Казалось бы, это предполагает, что ТОЛЬКО когда параметр значения равен нулю (или не указан), HtmlHelper ищет другое значение в другом месте.
В моем приложении у меня есть форма, где: html.Hidden("remote", True) отображается как <input id="remote" name="remote" type="hidden" value="False" />
Обратите внимание, что значение переопределяется тем, что находится в словаре ViewData.ModelState.
Или я что-то упустил?
Существует обходной путь:
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());
}
}
Так что в 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?
Как предлагали другие, я пошел с использованием прямого 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)