フォーム値が置き換えられるという ASP.NET MVC のバグの可能性
-
09-09-2019 - |
質問
ASP.NET MVC で問題が発生しているようです。ページ上に複数のフォームがあり、それぞれに同じ名前を使用しているが、異なるタイプ (ラジオ/非表示/など) として使用されている場合、最初のフォームの投稿 (たとえば、「日付」ラジオ ボタンを選択します)、フォームが再レンダリングされると (結果ページの一部として)、他のフォームの 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 を持っている他の人がこれを確認できますか?
enum を使用しているためかもしれません。わからない。隠しフィールドに「manual」input () タグを使用することでこの問題を回避できることを付け加えておきますが、MVC タグ (<%= Html.Hidden(...) %>) を使用すると、.NET MVC がそれらを置き換えます。毎回。
どうもありがとう。
アップデート:
今日またこのバグを見ました。これは、投稿されたページを返し、Html ヘルパーで MVC セットの隠しフォーム タグを使用すると、頭を切り取るようです。連絡しました フィル・ハーク これについては、他にどこに頼ればよいのか分からず、これが David が指定した予期される動作であるとは思えないからです。
解決
はい、この動作は現時点では仕様です。値を明示的に設定している場合でも、同じ URL にポストバックすると、モデルの状態が調べられ、そこにある値が使用されます。一般に、これにより、元の値ではなく、ポストバック時に送信された値を表示できます。
考えられる解決策は 2 つあります。
解決策 1
各フィールドには一意の名前を使用します。デフォルトでは、指定した名前が HTML 要素の ID として使用されることに注意してください。複数の要素に同じ ID を持たせるのは無効な HTML です。したがって、一意の名前を使用することをお勧めします。
解決策 2
Hidden ヘルパーは使用しないでください。本当に必要ないようですね。代わりに、次のようにすることもできます。
<input type="hidden" name="the-name"
value="<%= Html.AttributeEncode(Model.Value) %>" />
もちろん、これについてよく考えてみると、ポストバックに基づいて値を変更することは、テキストボックスでは意味がありますが、非表示の入力ではあまり意味がありません。v1.0 ではこれを変更できませんが、v2 では検討します。しかし、私たちはそのような変化がもたらす影響を注意深く考える必要があります。
他のヒント
他の人たちと同じように、モデルを埋めるために ModelState が使用されることを期待していました。ビュー内の式でモデルを明示的に使用するため、ModelState ではなく Model を使用する必要があります。
これはデザイン上の選択であり、その理由はわかります。検証が失敗した場合、入力値をモデル内のデータ型に解析できない可能性がありますが、ユーザーが入力した間違った値をレンダリングする必要があるため、それを修正するのは簡単です。
私が理解できない唯一のことは次のとおりです。 Model が使用されるのは設計上ではないのはなぜですか。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 を ... に置き換えます。
- 設計上の問題を回避するには、デフォルトの MVC HTML ヘルパーを置き換えるカスタム HTML ヘルパーを作成します。これは HTML をレンダリングするよりも一般的なアプローチですが、他のすべての機能を維持しながら Model に対する ModelState の優先順位を無効にするには、さらに HTML+MVC の知識が必要になるか、System.Web.MVC を逆コンパイルする必要があります。
- POST-REDIRECT-GET パターンを適用します。これは一部の環境では簡単ですが、より多くの相互作用や複雑さがある環境ではより困難になります。このパターンには長所と短所があり、設計により Model ではなく ModelState が選択されるため、このパターンの適用を強制されるべきではありません。
問題
したがって、問題は、モデルが ModelState から入力され、モデルを使用するように明示的に設定したビューに入力されることです。誰もが、検証エラーがない限り、Model 値が (変更された場合に備えて) 使用されることを期待しています。その後、ModelState を使用できます。
現在、MVC ヘルパー拡張機能では、ModelState 値が Model 値よりも優先されます。
解決
したがって、この問題の実際の修正は次のようになります。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 が通常提供するものを逆コンパイル、分析、再構築する必要はありません (時間の経過による変更、ブラウザーの違いなどの管理も忘れないでください)。
というロジックだと思います 「モデル値、検証エラーがない場合は ModelState」 設計上のはずだった。もしそうなら、これほど多くの人が巻き込まれることはなかったでしょうが、それでも MVC が意図していることはカバーされていました。
同じ問題に遭遇しました。TextBox() のような HTML ヘルパーの、渡された値の優先順位は、私が推測したものとまったく逆に動作するようです。 ドキュメンテーション そこにはこう書かれています:
テキスト入力要素の値。この値がnull参照(Visual Basicには何もありません)の場合、要素の値はViewDatadictionaryオブジェクトから取得されます。値が存在しない場合、値はModelStatedictionaryオブジェクトから取得されます。
私には、渡された場合にはその値が使用されると読みました。ただし、TextBox() ソースを読むと、次のようになります。
string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);
実際の順序が文書化されている順序と正反対であることを示しているようです。実際の順序は次のようになります。
- モデルの状態
- データの表示
- 値 (呼び出し元によって TextBox() に渡される)
注意 - このバグは MVC 3 にもまだ存在します。私は Razor マークアップ構文を使用しています (これは非常に重要です)。しかし、オブジェクト プロパティに毎回同じ値を生成する foreach ループで同じバグに遭遇しました。
これは予期される動作です。MVC は、フォームに追加情報を渡すためにビューステートやその他の裏技を使用しないため、どのフォームが送信されたのかわかりません (フォーム名は送信されたデータの一部ではありません。名前と値のペアのリスト)。
MVC がフォームをレンダリングして返すとき、同じ名前を持つ送信された値が存在するかどうかを確認するだけです。繰り返しになりますが、名前付きの値がどのフォームから来たのか、あるいはそれがどのようなタイプのコントロールであったかさえ知る方法がありません (ラジオ、テキスト、または非表示を使用します。HTTP 経由で送信される場合は、すべて単なる name=value です)。
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);
「設計上の問題」を再現する例と考えられる回避策。ただし、「バグ」を見つけるために失われた 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 ) %>
(フォームがレンダリングされたときの) "名前" の元の値が "dummy" の場合、フォームが送信された後、"dummy (user set)" がレンダリングされることが期待されます。それなし ModelState.Clear()
まだ「ダミー」が表示されます !!!!!!
正しい回避策:
<input type="hidden" name="Name" value="<%= Html.AttributeEncode(Model.Name) %>" />
すべての MVC フォーム開発者はこの点を念頭に置く必要があるため、これはまったく良い設計ではないと感じています。
この問題は 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));
}
これは「仕様」である可能性がありますが、文書化されたものではありません。
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 のメンバー
まとめ:非表示の入力タグを返します。
パラメーター:
htmlヘルパー:HTML ヘルパー。
名前:値の検索に使用されるフォーム フィールド名と System.Web.Mvc.ViewDataDictionary キー。
価値:非表示の入力の値。null の場合は、System.Web.Mvc.ViewDataDictionary を調べてから、System.Web.Mvc.ModelStateDictionary の値を調べます。
これは、value パラメーターが null (または指定されていない) の場合にのみ、HtmlHelper が値を別の場所で検索することを示唆しているように見えます。
私のアプリには次のようなフォームがあります。html.Hidden("remote", True) は次のようにレンダリングされます<input id="remote" name="remote" type="hidden" value="False" />
この値は ViewData.ModelState ディクショナリの内容によって上書きされることに注意してください。
それとも何かが足りないのでしょうか?
つまり、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 で修正されましたか?
回避策があります:
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());
}
}
他の人が提案したように、私は 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)">
アップデート:これは便利な 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)