Posible error en ASP.NET MVC con valores de formulario que se reemplazan

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.

             

Origen de la página resultante (esto sería parte de la página de resultados)

 

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 tags de entrada “manual” () para los campos ocultos, pero si uso tags MVC (), .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 tags 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.

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:

  

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.

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 descomstackr 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í:

  ///  /// 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. ///  public static void RemoveStateFor(this HtmlHelper helper, Expression> 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(this HtmlHelper htmlHelper, Expression> expression, string format = "", Dictionary htmlAttributes = null) { RemoveStateFor(htmlHelper, expression); return htmlHelper.TextBoxFor(expression, format, htmlAttributes); } public static IHtmlString HiddenForModel(this HtmlHelper htmlHelper, Expression> 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.

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 Documentación 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:

  1. ModelState
  2. Ver datos
  3. Valor (pasado a TextBox () por la persona que llama)

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

Heads-up: este error todavía existe en MVC 3. Estoy usando la syntax 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.

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

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:

  

Siento que este no es un buen diseño en absoluto, ya que todos los desarrolladores de formularios mvc deben tenerlo en cuenta.

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:

  ///  /// 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. ///  public static IHtmlString HiddenTheWayWeWantItFor(this HtmlHelper htmlHelper, Expression> expression, object value = null, bool withValidation = false) { if (value == null) { value = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model; } return new HtmlString(String.Format("", htmlHelper.IdFor(expression), htmlHelper.NameFor(expression), value)); } 

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

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 = @""; public static MvcHtmlString HiddenEx(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()); } } 

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++) {  // Takes the recent value changed in the controller! } 

¿Solucionaron esto en MVC 5?

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.

  

Actualización: aquí hay una útil extensión HtmlHelper

  public static MvcHtmlString MyHiddenFor(this HtmlHelper helper, Expression> expression, object htmlAttributes = null) { return new MvcHtmlString( string.Format( @"", helper.IdFor(expression), helper.NameFor(expression), GetValueFor(helper, expression) )); } ///  /// Retrieves value from expression ///  private static string GetValueFor(HtmlHelper helper, Expression> 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)