web-dev-qa-db-fra.com

Bug possible dans ASP.NET MVC avec remplacement des valeurs de formulaire

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.

40
Dan Atkinson

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:

Solution 1

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.

Solution 2

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.

35
Haacked

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 

  • ModelState.Clear (): Efface toutes les valeurs ModelState, mais désactive fondamentalement l'utilisation de la validation par défaut dans MVC.
  • ModelState.Remove ("SomeKey"): Identique à ModelState.Clear () mais nécessite une microgestion des clés ModelState, ce qui représente une charge de travail trop importante et qui ne convient pas à la fonction de liaison automatique de MVC. On se sent comme il y a 20 ans, quand on gérait aussi des clés Form et QueryString.
  • Rendu HTML eux-mêmes: trop de travail, de détails et le rejet des méthodes HTML Helper avec les fonctionnalités supplémentaires. Un exemple: Remplacez @ Html.HiddenFor par m.Name) "id =" @ Html.IdFor (m => m.Name) "value =" @ Html.AttributeEncode (Model.Name) ">. Ou remplacez @ Html.DropDownListFor par ...
  • Créez des assistants HTML personnalisés pour remplacer les assistants HTML par défaut de MVC afin d'éviter le problème de conception indirecte. Il s'agit d'une approche plus générique que le rendu de votre code HTML, mais qui nécessite encore davantage de connaissances HTML + MVC ou la décompilation de System.Web.MVC pour conserver toutes les autres fonctionnalités, mais désactiver la priorité de ModelState sur Model.
  • Appliquez le modèle POST-REDIRECT-GET: cela est facile dans certains environnements, mais plus difficile dans ceux avec plus d'interaction/complexité. Ce modèle a ses avantages et ses inconvénients et vous ne devriez pas être obligé de l'appliquer en raison d'un choix de DesignState par rapport au modèle. 

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.

10
Dacker

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:

  1. Etat du modèle
  2. ViewData
  3. Valeur (transmise à TextBox () par l'appelant)
6
user183460

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

5
David

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.

5
Mark S
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);
4
jorge

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.

3
Softlion

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 correcte

Notre 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));
        }
2
BlackjacketMack

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

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?

1
Darragh

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)
0
jBelanger

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?

0
Juan Carlos Puerto