asp.net-mvc - una - formulario mvc c#
Posible error en ASP.NET MVC con valores de formulario que se reemplazan (12)
Parece que tengo un problema con ASP.NET MVC en eso, si tengo más de un formulario en una página que usa el mismo nombre en cada uno, pero como diferentes tipos (radio / oculto / etc.), entonces, cuando el primeras publicaciones de formulario (elijo el botón de opción ''Fecha'', por ejemplo), si el formulario se re-renderiza (por ejemplo, como parte de la página de resultados), parece que tengo el problema de que el valor oculto de SearchType en las otras formas se cambia al último valor del botón de opción (en este caso, SearchType.Name).
A continuación se muestra un formulario de ejemplo para fines de reducción.
<% 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(); %>
Origen de la página resultante (esto sería parte de la página de resultados)
<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>
Por favor, ¿alguien más con RC1 puede confirmar esto?
Tal vez es porque estoy usando una enumeración. No lo sé. Debo añadir que puedo eludir este problema usando etiquetas de entrada "manual" () para los campos ocultos, pero si uso etiquetas MVC (<% = Html.Hidden (...)%>), .NET MVC las reemplaza cada vez.
Muchas gracias.
Actualizar:
He visto este error otra vez hoy. Parece que esto sale mal cuando devuelves una página publicada y utilizas las etiquetas de formulario ocultas de MVC con el ayudante Html. Me puse en contacto con Phil Haack sobre esto, porque no sé a dónde más recurrir, y no creo que este comportamiento sea el esperado como lo especifica David.
Al igual que otros, esperaba que ModelState se utilizara para llenar el Modelo y, como usamos explícitamente el Modelo en las expresiones de la vista, debería usar el Modelo y no el Estado del Modelo.
Es una elección de diseño y entiendo por qué: si las validaciones fallan, el valor de entrada puede no ser analizable para el tipo de datos en el modelo y aún así se desea representar el valor incorrecto que el usuario tipeó, por lo que es fácil corregirlo.
Lo único que no entiendo es: ¿por qué no se utiliza el modelo, que el desarrollador establece explícitamente y, si se produce un error de validación, se utiliza el modelo StateState?
He visto a mucha gente usando soluciones como
- ModelState.Clear (): borra todos los valores de ModelState, pero básicamente deshabilita el uso de la validación por defecto en MVC
- ModelState.Remove ("SomeKey"): Igual que ModelState.Clear (), pero necesita una microgestión de las claves de ModelState, que es demasiado trabajo y no se siente bien con la función de vinculación automática de MVC. Se siente como hace 20 años cuando también manejábamos las claves Form y QueryString.
- Render HTML ellos mismos: demasiado trabajo, detalle y arroja los métodos de HTML Helper con las características adicionales. Un ejemplo: Reemplace @ Html.HiddenFor por m.Name) "id =" @ Html.IdFor (m => m.Name) "value =" @ Html.AttributeEncode (Model.Name) ">. O reemplace @Html. DropDownListFor por ...
- Crea HTML Helpers personalizados para reemplazar MVC HTML Helpers predeterminados para evitar el problema del diseño. Este es un enfoque más genérico que el renderizado de HTML, pero aún requiere más conocimiento de HTML + MVC o descompilar System.Web.MVC para conservar todas las demás características pero deshabilitar la precedencia de ModelState sobre Model.
- Aplicar el patrón POST-REDIRECT-GET: esto es fácil en algunos entornos, pero más difícil en los que tienen más interacción / complejidad. Este patrón tiene sus pros y sus contras y no se le debe obligar a aplicar este patrón debido a una elección por diseño de ModelState sobre el modelo.
Problema
Entonces, el problema es que el Modelo se llena desde ModelState y en la vista que configuramos explícitamente para usar el Modelo. Todo el mundo espera que se use el valor del Modelo (en caso de que haya cambiado), a menos que haya un error de validación; luego se puede usar el ModeloEstado.
Actualmente, en las extensiones de MVC Helper, el valor de ModelState tiene prioridad sobre el valor del Modelo.
Solución
Por lo tanto, la solución real para este problema debería ser: para que cada expresión extraiga el valor del Modelo, el valor de ModelState debe eliminarse si no hay un error de validación para ese valor. Si hay un error de validación para ese control de entrada, el valor de ModelState no se debe eliminar y se usará como lo hace normalmente. Creo que esto resuelve el problema exactamente, que es mejor que la mayoría de las soluciones.
El código está aquí:
/// <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);
}
}
Y luego creamos nuestras propias extensiones HTML Helper todo esto antes de llamar a las extensiones 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);
}
Esta solución elimina el problema, pero no requiere que descompile, analice y reconstruya lo que MVC le ofrece normalmente (no olvide administrar los cambios a lo largo del tiempo, las diferencias del navegador, etc.).
Creo que la lógica de "Valor del modelo, a menos que haya un error de validación luego ModelState" debería haber sido un diseño. Si lo fuera, no habría mordido a tanta gente, pero aún cubría lo que MVC pretendía hacer.
Como otros han sugerido, utilicé el código html directo en lugar de utilizar HtmlHelpers (TextBoxFor, CheckBoxFor, HiddenFor, etc.).
Sin embargo, el problema con este enfoque es que debe poner los atributos de nombre e id como cadenas. Quería mantener las propiedades de mi modelo fuertemente tipadas, así que usé NameFor e IdFor HtmlHelpers.
<input type="hidden" name="@Html.NameFor(m => m.Name)" id="@Html.IdFor(m=>m.Name)" value="@Html.AttributeEncode(Model.Name)">
Actualización: aquí hay una útil extensión 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;
}
Puede usarlo como
@Html.MyHiddenFor(m => m.Name)
Ejemplo para reproducir el "problema de diseño" y un posible workaroud. No hay ninguna solución para las 3 horas perdidas tratando de encontrar el "error" ... Tenga en cuenta que este "diseño" todavía está en 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);
}
Si su formulario contiene originalmente esto: <%= Html.HiddenFor( m => m.ProductId ) %>
Si el valor original de "Nombre" (cuando se procesó el formulario) es "ficticio", después de enviar el formulario, espera ver "ficticio (conjunto de usuario)". Sin ModelState.Clear()
aún verás "dummy" !!!!!!
Solución correcta:
<input type="hidden" name="Name" value="<%= Html.AttributeEncode(Model.Name) %>" />
Siento que este no es un buen diseño en absoluto, ya que todos los desarrolladores de formularios mvc deben tenerlo en cuenta.
Entonces en MVC 4 el "problema de diseño" sigue ahí. Este es el código que tuve que usar para establecer los valores ocultos correctos en una colección, ya que independientemente de lo que haga en el controlador, la vista siempre mostraba valores incorrectos.
Código VIEJO
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
}
Código ACTUALIZADO
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!
}
¿Solucionaron esto en MVC 5?
Este problema aún existe en MVC 5, y claramente no se considera un error que está bien.
Estamos descubriendo que, aunque por diseño, este no es el comportamiento esperado para nosotros. Por el contrario, siempre queremos que el valor del campo oculto funcione de manera similar a otros tipos de campos y no se trate de manera especial, o que extraigamos su valor de una colección oscura (¡lo que nos recuerda a ViewState!).
Algunos hallazgos (el valor correcto para nosotros es el valor del modelo, el valor de ModelState es incorrecto):
-
Html.DisplayFor()
muestra el valor correcto (extrae del modelo) -
Html.ValueFor
no (extrae de ModelState) -
ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model
extrae el valor correcto
Nuestra solución es simplemente implementar nuestra propia Extensión:
/// <summary>
/// Custom HiddenFor that addresses the issues noted here:
/// http://.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));
}
Este sería el comportamiento esperado: MVC no usa un viewstate u otro detrás de tus trucos para pasar información adicional en el formulario, por lo que no tiene idea de qué formulario enviaste (el nombre del formulario no es parte de los datos enviados, solo una lista de pares nombre / valor).
Cuando MVC devuelve el formulario, simplemente verifica si existe un valor enviado con el mismo nombre; una vez más, no tiene forma de saber de qué forma procede un valor con nombre, ni siquiera de qué tipo de control era (independientemente de que usar una radio, texto u oculto, todo es solo nombre = valor cuando se envía a través de HTTP).
Esto puede ser ''por diseño'', pero no es lo que está documentado:
Public Shared Function Hidden( ByVal htmlHelper As System.Web.Mvc.HtmlHelper, ByVal name As String, ByVal value As Object) As String
Miembro de System.Web.Mvc.Html.InputExtensions
Resumen: devuelve una etiqueta de entrada oculta.
Parámetros:
htmlHelper: El ayudante HTML.
name: el nombre del campo de formulario y la clave System.Web.Mvc.ViewDataDictionary utilizados para buscar el valor.
valor: el valor de la entrada oculta. Si es nulo, mira System.Web.Mvc.ViewDataDictionary y luego System.Web.Mvc.ModelStateDictionary para el valor.
Esto parece sugerir que SÓLO cuando el parámetro de valor sea nulo (o no se especifique), el HtmlHelper buscará un valor en otro lugar.
En mi aplicación, tengo un formulario donde: html.Hidden ("remote", True) se representa como <input id="remote" name="remote" type="hidden" value="False" />
Tenga en cuenta que el valor está siendo sobrepasado por lo que está en el diccionario ViewData.ModelState.
¿O me estoy perdiendo algo?
Hay una solución alternativa:
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());
}
}
Heads-up: este error todavía existe en MVC 3. Estoy usando la sintaxis de marcado Razor (como eso realmente importa), pero encontré el mismo error con un bucle Foreach que producía el mismo valor para una propiedad de objeto cada vez.
Me encontré con el mismo problema. Ayudantes de HTML como la precedencia de TextBox () para valores pasados parecen comportarse exactamente en oposición a lo que deduje de la Documentation donde dice:
El valor del elemento de entrada de texto. Si este valor es referencia nula (Nothing en Visual Basic), el valor del elemento se recupera del objeto ViewDataDictionary. Si no existe ningún valor allí, el valor se recupera del objeto ModelStateDictionary.
Para mí, he leído que se usa el valor, si se pasa. Pero leyendo la fuente de TextBox ():
string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);
parece indicar que el orden real es exactamente lo contrario de lo que está documentado. El orden real parece ser:
- ModelState
- Ver datos
- Valor (pasado a TextBox () por la persona que llama)
Sí, este comportamiento es actualmente por diseño. Aunque está estableciendo valores explícitamente, si publica de nuevo en la misma URL, buscamos en el estado del modelo y usamos el valor allí. En general, esto nos permite mostrar el valor que envió en la devolución de datos, en lugar del valor original.
Hay dos soluciones posibles:
Solución 1
Use nombres únicos para cada uno de los campos. Tenga en cuenta que, de manera predeterminada, utilizamos el nombre que especifique como ID del elemento HTML. Es HTML no válido tener múltiples elementos con el mismo ID. Entonces, usar nombres únicos es una buena práctica.
Solución 2
No uses el Ayudante oculto. Parece que realmente no lo necesitas. En cambio, podrías hacer esto:
<input type="hidden" name="the-name"
value="<%= Html.AttributeEncode(Model.Value) %>" />
Por supuesto, cuando pienso en esto más, cambiar el valor basado en una devolución de datos tiene sentido para los cuadros de texto, pero tiene menos sentido para las entradas ocultas. No podemos cambiar esto por v1.0, pero lo consideraré para v2. Pero tenemos que pensar cuidadosamente las implicaciones de tal cambio.
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);