Il semble que je rencontre un problème avec ASP.NET MVC en ce sens que, si j’ai plus d’un formulaire sur une page qui utilise le même nom dans chaque formulaire, mais sous différents types (radio/hidden/etc), premiers messages sous forme de formulaire (je choisis le bouton radio "Date" par exemple), si le formulaire est rendu (par exemple dans le cadre de la page de résultats), il me semble que le problème est que la valeur cachée du SearchType sur les autres formulaires est remplacé par la dernière valeur du bouton radio (dans ce cas, SearchType.Name).
Vous trouverez ci-dessous un exemple de formulaire à des fins de réduction.
<% 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(); %>
Source de page résultante (cela ferait partie de la page de résultats)
<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>
S'il vous plaît quelqu'un d'autre avec RC1 peut confirmer cela?
C'est peut-être parce que j'utilise un enum. Je ne sais pas. Je devrais ajouter que je peux contourner ce problème en utilisant des balises input () 'manuelles' pour les champs masqués, mais si j'utilise des balises MVC (<% = Html.Hidden (...)%>), .NET MVC les remplace à chaque fois.
Merci beaucoup.
Mise à jour:
J'ai encore vu ce bogue aujourd'hui. Il semble que cela coupe la tête lorsque vous renvoyez une page publiée et que vous utilisez les balises de formulaire masquées définies avec MVC avec l’aide HTML. J'ai contacté Phil Haack à ce sujet, car je ne sais pas vers qui se tourner, et je ne crois pas que cela devrait être le comportement attendu, comme l'a spécifié David.
Oui, ce comportement est actuellement voulu. Même si vous définissez explicitement des valeurs, si vous publiez de nouveau sur la même URL, nous examinons l'état du modèle et utilisons la valeur à cet endroit. En général, cela nous permet d'afficher la valeur que vous avez soumise lors de la publication, plutôt que la valeur d'origine.
Il y a deux solutions possibles:
Utilisez des noms uniques pour chacun des champs. Notez que par défaut, nous utilisons le nom que vous spécifiez en tant qu'id de l'élément HTML. Ce n'est pas HTML valide d'avoir plusieurs éléments ayant le même identifiant. Donc, utiliser des noms uniques est une bonne pratique.
N'utilisez pas l'assistant caché. Il semble que vous n'en ayez vraiment pas besoin. Au lieu de cela, vous pouvez faire ceci:
<input type="hidden" name="the-name"
value="<%= Html.AttributeEncode(Model.Value) %>" />
Bien entendu, à mesure que j'y réfléchis davantage, modifier la valeur en fonction d'une publication a du sens pour les zones de texte, mais moins pour les entrées masquées. Nous ne pouvons pas changer cela pour la v1.0, mais je le considérerai pour la v2. Mais nous devons réfléchir soigneusement aux implications d'un tel changement.
Identique aux autres, je m'attendais à ce que ModelState soit utilisé pour remplir le modèle et comme nous utilisons explicitement le modèle dans les expressions de la vue, il doit utiliser Model et non ModelState.
C'est un choix de conception et je comprends pourquoi: si les validations échouent, la valeur en entrée risque de ne pas être analysable par rapport au type de données du modèle et vous souhaitez toujours restituer la valeur erronée saisie par l'utilisateur, il est donc facile de la corriger.
La seule chose que je ne comprends pas, c’est: pourquoi ce n’est pas par conception que le modèle est utilisé, ce qui est défini explicitement par le développeur et si une erreur de validation se produit, le ModelState est utilisé.
J'ai vu beaucoup de gens utiliser des solutions de contournement comme
Problème
Le problème est donc que le modèle est rempli à partir de ModelState et dans la vue que nous avons explicitement définie pour utiliser le modèle. Tout le monde s'attend à ce que la valeur du modèle (au cas où elle aurait changé) soit utilisée, sauf en cas d'erreur de validation. alors le ModelState peut être utilisé.
Actuellement, dans les extensions MVC Helper, la valeur ModelState est prioritaire sur la valeur Model.
Solution
Le correctif actuel de ce problème doit donc être le suivant: pour chaque expression permettant d'extraire la valeur Model, la valeur ModelState doit être supprimée s'il n'y a pas d'erreur de validation pour cette valeur. S'il y a une erreur de validation pour ce contrôle d'entrée, la valeur ModelState ne doit pas être supprimée et sera utilisée comme d'habitude. Je pense que cela résout le problème avec précision, ce qui est préférable à la plupart des solutions de contournement.
Le code est ici:
/// <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);
}
}
Et ensuite, nous créons nos propres extensions HTML Helper à faire avant d’appeler les extensions 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);
}
Cette solution supprime le problème, mais ne vous oblige pas à décompiler, analyser et reconstruire ce que MVC vous offre normalement (n'oubliez pas également de gérer les modifications au fil du temps, les différences de navigateur, etc.).
Je pense que la logique de "Valeur du modèle, sauf erreur de validation, alors ModelState" aurait dû être une conception indirecte. Si c’était le cas, il n’aurait pas mordu autant de gens, mais il aurait quand même couvert ce que MVC était censé faire.
Je viens de rencontrer le même problème. Les aides HTML telles que la priorité TextBox () pour les valeurs passées semblent se comporter exactement de la même manière que ce que j'ai déduit de Documentation où il est indiqué:
La valeur de l'élément d'entrée de texte. Si cette valeur est null référence (Rien dans Visual Basic), la valeur de l'élément est extraite de De l'objet ViewDataDictionary. S'il n'y a aucune valeur, la valeur est Extraite de l'objet ModelStateDictionary.
Pour moi, j'ai lu que la valeur, si passée, est utilisée. Mais en lisant la source TextBox ():
string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);
semble indiquer que l'ordre réel est l'exact opposé de ce qui est documenté. L'ordre réel semble être:
Ce serait le comportement attendu - MVC n'utilise pas d'état View ou autre derrière vos trucs en arrière pour transmettre des informations supplémentaires dans le formulaire. Il n'a donc aucune idée du formulaire que vous avez soumis (le nom du formulaire ne fait pas partie des données soumises, mais uniquement une liste de paires nom/valeur).
Lorsque MVC restitue le formulaire, il vérifie simplement si une valeur soumise portant le même nom existe. Encore une fois, il n’a aucun moyen de savoir de quelle forme provient une valeur nommée, ni même de quel type de contrôle utiliser une radio, texte ou caché, tout est juste nom = valeur quand il est soumis via HTTP).
Heads-up - ce bogue existe toujours dans MVC 3. J'utilise la syntaxe de balisage Razor (comme si cela comptait vraiment), mais j'ai rencontré le même bogue avec une boucle foreach qui produisait la même valeur pour une propriété d'objet à chaque fois.
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);
Exemple pour reproduire le "problème de conception" et une solution possible. Il n’existe aucune solution de contournement pour les 3 heures perdues, alors que vous cherchiez le "bogue" bien que ... Notez que cette "conception" est toujours dans 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 votre formulaire contient à l'origine ceci: <%= Html.HiddenFor( m => m.ProductId ) %>
Si la valeur d'origine de "Nom" (lors du rendu du formulaire) est "fictive", une fois le formulaire soumis, vous vous attendez à voir "fictif (utilisateur défini)". Sans ModelState.Clear()
, vous verrez toujours "factice" !!!!!!
Solution de contournement correcte:
<input type="hidden" name="Name" value="<%= Html.AttributeEncode(Model.Name) %>" />
Je pense que ce n'est pas du tout une bonne conception, car chaque développeur de formulaire MVC doit en tenir compte.
Ce problème existe toujours dans MVC 5 et, de toute évidence, il n’est pas considéré comme un bogue qui convient.
Nous constatons que, bien que ce soit intentionnel, ce n'est pas le comportement attendu pour nous. Au contraire, nous souhaitons toujours que la valeur du champ masqué fonctionne de la même manière que d’autres types de champs et ne soit pas traitée comme telle, ou tire sa valeur d’une collection obscure (qui nous rappelle ViewState!).
Quelques constatations (la valeur correcte pour nous est la valeur du modèle, incorrecte est la valeur ModelState):
Html.DisplayFor()
affiche la valeur correcte (extrait du modèle) Html.ValueFor
ne le fait pas (il provient de ModelState)ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model
extrait la valeur correcteNotre solution consiste simplement à mettre en œuvre notre propre extension:
/// <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));
}
Il y a une solution de contournement:
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());
}
}
Cela peut être "à dessein" mais ce n'est pas ce qui est documenté:
Public Shared Function Hidden( ByVal htmlHelper As System.Web.Mvc.HtmlHelper, ByVal name As String, ByVal value As Object) As String
Membre de System.Web.Mvc.Html.InputExtensions
Résumé: renvoie une balise d'entrée masquée.
Paramètres:
htmlHelper: L'assistant HTML.
name: nom du champ du formulaire et clé System.Web.Mvc.ViewDataDictionary utilisé pour rechercher la valeur.
valeur: valeur de l'entrée masquée. Si la valeur est null, examine System.Web.Mvc.ViewDataDictionary, puis System.Web.Mvc.ModelStateDictionary pour connaître la valeur.
Cela semblerait suggérer que UNIQUEMENT lorsque le paramètre value est nul (ou non spécifié), HtmlHelper cherche une valeur ailleurs.
Dans mon application, j'ai un formulaire où: html.Hidden ("remote", True) s'affiche en tant que <input id="remote" name="remote" type="hidden" value="False" />
Notez que la valeur est remplacée par ce qui se trouve dans le dictionnaire ViewData.ModelState.
Ou est-ce que je manque quelque chose?
Comme d'autres l'ont suggéré, j'ai utilisé du code HTML direct au lieu d'utiliser HtmlHelpers (TextBoxFor, CheckBoxFor, HiddenFor, etc.).
Le problème cependant avec cette approche est que vous devez mettre les attributs name et id sous forme de chaînes. Je voulais que les propriétés de mon modèle restent fortement typées et j'ai donc utilisé les outils NameFor et IdFor HtmlHelpers.
<input type="hidden" name="@Html.NameFor(m => m.Name)" id="@Html.IdFor(m=>m.Name)" value="@Html.AttributeEncode(Model.Name)">
Mise à jour: Voici une extension pratique de 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;
}
Vous pouvez ensuite l'utiliser comme
@Html.MyHiddenFor(m => m.Name)
Donc, dans MVC 4, le "problème de conception" existe toujours. Voici le code que j'ai dû utiliser pour définir les valeurs masquées correctes dans une collection, car, quel que soit ce que je fais dans le contrôleur, la vue affichait toujours des valeurs incorrectes.
Ancien code
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
}
Code mis à jour
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!
}
L'ont-ils corrigé dans MVC 5?