web-dev-qa-db-ja.com

フォーム値が置き換えられるASP.NET MVCのバグの可能性

ASP.NET MVCで問題が発生しているようです。ページに複数のフォームがあり、それぞれに同じ名前を使用しているが、タイプが異なる(radio/hidden/etc)場合、最初のフォームの投稿(たとえば、[日付]ラジオボタンを選択します)、フォームが(たとえば、結果ページの一部として)再レンダリングされる場合、他のフォームの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を持っている他の誰かがこれを確認できますか?

多分それは私が列挙型を使用しているためです。知りません。隠しフィールドに「手動」入力()タグを使用することでこの問題を回避できることを追加する必要がありますが、MVCタグ(<%= Html.Hidden(...)%>)を使用すると、.NET MVCによって置き換えられます毎回。

どうもありがとう。

更新:

今日もこのバグを見ました。これは、投稿されたページを返し、MVCセットの非表示フォームタグをHtmlヘルパーで使用すると頭が切り取られるようです。これについて Phil Haack に連絡しました。他にどこを曲がればよいのかわからないためです。また、Davidの指定どおりにこれが期待される動作であるとは思わないからです。

41
Dan Atkinson

はい、この動作は現在仕様によるものです。明示的に値を設定している場合でも、同じURLにポストバックすると、モデルの状態が調べられ、そこで値が使用されます。通常、これにより、元の値ではなく、ポストバックで送信した値を表示できます。

2つの可能な解決策があります。

解決策1

各フィールドに一意の名前を使用します。デフォルトでは、指定した名前をHTML要素のIDとして使用することに注意してください。複数の要素が同じIDを持つことは無効なHTMLです。したがって、一意の名前を使用することをお勧めします。

解決策2

非表示ヘルパーは使用しないでください。あなたは本当にそれを必要としないようです。代わりに、これを行うことができます:

<input type="hidden" name="the-name" 
  value="<%= Html.AttributeEncode(Model.Value) %>" />

もちろん、これについてもっと考えると、ポストバックに基づいて値を変更することは、Textboxには意味がありますが、非表示の入力には意味がありません。 v1.0の場合は変更できませんが、v2の場合は検討します。しかし、そのような変化の影響を注意深く検討する必要があります。

36
Haacked

他のモデルと同じように、ModelStateを使用してモデルを埋めることを期待していました。ビューの式で明示的にModelを使用するため、ModelStateではなくModelを使用する必要があります。

これは設計上の選択であり、理由はわかります。検証が失敗した場合、入力値はモデルのデータ型に解析できない可能性があり、ユーザーが入力した間違った値をレンダリングしたいので、簡単に修正できます。

私が理解できない唯一のことは次のとおりです:開発者が明示的に設定するモデルが使用され、検証エラーが発生した場合は、ModelStateが使用されるのは設計上そうではない理由です。

多くの人が次のような回避策を使用しているのを見てきました

  • ModelState.Clear():すべてのModelState値をクリアしますが、基本的にはMVCでのデフォルトの検証の使用を無効にします
  • ModelState.Remove( "SomeKey"):ModelState.Clear()と同じですが、ModelStateキーのマイクロ管理が必要です。これは作業量が多すぎて、MVCの自動バインディング機能で正しく動作しません。 FormとQueryStringキーも管理していた20年前のように感じます。
  • HTML自体のレンダリング:多くの作業、詳細、追加機能を備えたHTMLヘルパーメソッドを破棄します。例:@ Html.HiddenForをm.Nameで置き換える) "id =" @ Html.IdFor(m => m.Name) "value =" @ Html.AttributeEncode(Model.Name) ">。または@Htmlを置き換えます。 DropDownListFor by ...
  • カスタムHTMLヘルパーを作成して、デフォルトのMVC HTMLヘルパーを置き換え、設計上の問題を回避します。これは、HTMLをレンダリングするより一般的な方法ですが、他のすべての機能を維持しながらModelStateの優先順位を無効にするには、HTML + MVCの知識またはSystem.Web.MVCの逆コンパイルが必要です。
  • POST-REDIRECT-GETパターンを適用します。これは、一部の環境では簡単ですが、相互作用/複雑さが高い環境では難しくなります。このパターンには長所と短所があり、ModelではなくModelStateを設計上選択するため、このパターンの適用を強制されるべきではありません。

問題

したがって問題は、ModelがModelStateから入力され、ビューで明示的にModelを使用するように設定することです。検証エラーがない限り、誰もがモデル値(変更された場合)が使用されることを期待しています。その後、ModelStateを使用できます。

現在MVCヘルパー拡張機能では、ModelState値が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);
        }
    }

そして、MVC拡張を呼び出す前に、これを行うために独自のHTMLヘルパー拡張を作成します。

    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が通常提供しているものすべてを逆コンパイル、分析、再構築する必要はありません(経時的な変更、ブラウザーの違いなどの管理も忘れないでください)。

"Model value、not validation error then ModelState"のロジックは仕様によるものである必要があると思います。もしそうなら、それはそれほど多くの人々を噛まなかったでしょうが、それでもMVCが意図することをカバーしました。

11
Dacker

ヘッズアップ-このバグはMVC 3にも存在します。Razorマークアップ構文を使用しています(これは本当に重要です)が、毎回オブジェクトプロパティに同じ値を生成するforeachループで同じバグに遭遇しました。

6
Mark S

同じ問題が発生しました。渡された値のTextBox()優先順位のようなHTMLヘルパーは、 Documentation から推測したものとは正反対の動作をするようです:

テキスト入力要素の値。この値がnull参照(Visual BasicではNothing)の場合、要素の値はViewDataDictionaryオブジェクトから取得されます。そこに値が存在しない場合、値はModelStateDictionaryオブジェクトから取得されます。

私には、渡された場合の値が使用されることを読みました。しかし、TextBox()ソースを読み取る:

string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);

実際の順序が文書化されているものとは正反対であることを示しているようです。実際の順序は次のようです:

  1. ModelState
  2. ViewData
  3. 値(呼び出し元によってTextBox()に渡されます)
6
user183460

これは予想される動作です-MVCは、バックステートの背後にあるビューステートなどを使用してフォームに追加の情報を渡さないため、どのフォームを送信したかはわかりません(フォーム名は送信されたデータの一部ではなく、名前/値ペアのリスト)。

MVCがフォームをレンダリングするとき、同じ名前の送信された値が存在するかどうかを確認するだけです。ここでも、名前付きの値がどのフォームから来たのか、またはそれがどのタイプのコントロールであったのかを知る方法はありません(ラジオ、テキスト、または非表示を使用します。HTTP経由で送信された場合は、すべてname = valueです)。

5
David
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

この問題はまだ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://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));
        }
_
4
BlackjacketMack

「設計上の問題」を再現する例と、考えられる解決策。 「バグ」を見つけようとして失われた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フォーム開発者がそれを覚えておく必要があるので、これはまったく良いデザインではないと思います。

3
Softlion

回避策があります:

    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

これは「設計による」かもしれませんが、文書化されているものではありません。

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キー。
value:非表示の入力の値。 nullの場合、System.Web.Mvc.ViewDataDictionaryを調べ、次にSystem.Web.Mvc.ModelStateDictionaryで値を探します。

これは、値パラメーターがnull(または指定されていない)の場合にのみ、HtmlHelperが他の場所で値を探すことを示唆しているようです。

私のアプリでは、html.Hidden( "remote"、True)が<input id="remote" name="remote" type="hidden" value="False" />

値がViewData.ModelStateディクショナリにあるもので上書きされていることに注意してください。

それとも何か不足していますか?

1
Darragh

したがって、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でこれを修正しましたか?

1

他の人が示唆したように、私はHtmlHelpers(TextBoxFor、CheckBoxFor、HiddenForなど)を使用する代わりに直接HTMLコードを使用しました。

ただし、このアプローチの問題は、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)">

pdate:これは便利な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)
0
jBelanger