ASP.NET MVC ベータ 1:DefaultModelBinder が、無関係なリクエスト間でパラメータと検証状態を誤って永続化します。
-
04-07-2019 - |
質問
デフォルトのモデル バインディングを使用して、アクションのパラメーターである複合オブジェクトにフォーム パラメーターをバインドすると、フレームワークは最初のリクエストに渡された値を記憶します。つまり、そのアクションに対する後続のリクエストは最初のリクエストと同じデータを取得します。パラメータ値と検証状態は、無関係な Web リクエスト間で保持されます。
これが私のコントローラーコードです(service
アプリのバックエンドへのアクセスを表します):
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Create()
{
return View(RunTime.Default);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(RunTime newRunTime)
{
if (ModelState.IsValid)
{
service.CreateNewRun(newRunTime);
TempData["Message"] = "New run created";
return RedirectToAction("index");
}
return View(newRunTime);
}
私の .aspx ビュー (次のように厳密に型付けされています) ViewPage<RunTime
>) には次のようなディレクティブが含まれます。
<%= Html.TextBox("newRunTime.Time", ViewData.Model.Time) %>
これは、 DefaultModelBinder
クラス、つまり モデルのプロパティを自動バインドすることを目的としていました.
ページにアクセスし、有効なデータを入力しました (例:時間 = 1)。アプリは新しいオブジェクトを time = 1 で正しく保存します。次に、もう一度押して、別の有効なデータを入力します(例:時間 = 2)。ただし、保存されるデータはオリジナルです (例:時間 = 1)。これは検証にも影響するため、元のデータが無効だった場合、今後入力するデータはすべて無効とみなされます。IIS を再起動するか、コードを再構築すると、永続化された状態がフラッシュされます。
この問題は、独自のハードコードされたモデル バインダーを作成することで解決できます。その基本的な単純な例を以下に示します。
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([ModelBinder(typeof (RunTimeBinder))] RunTime newRunTime)
{
if (ModelState.IsValid)
{
service.CreateNewRun(newRunTime);
TempData["Message"] = "New run created";
return RedirectToAction("index");
}
return View(newRunTime);
}
internal class RunTimeBinder : DefaultModelBinder
{
public override ModelBinderResult BindModel(ModelBindingContext bindingContext)
{
// Without this line, failed validation state persists between requests
bindingContext.ModelState.Clear();
double time = 0;
try
{
time = Convert.ToDouble(bindingContext.HttpContext.Request[bindingContext.ModelName + ".Time"]);
}
catch (FormatException)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName + ".Time", bindingContext.HttpContext.Request[bindingContext.ModelName + ".Time"] + "is not a valid number");
}
var model = new RunTime(time);
return new ModelBinderResult(model);
}
}
何かが足りないのでしょうか?最初のデータが 1 つのブラウザに入力され、2 番目のデータが別のブラウザに入力された場合に問題が再現できるため、ブラウザ セッションの問題ではないと思います。
解決
問題は、呼び出し間でコントローラーが再利用されていることでした。元の投稿から省略した詳細の1つは、Castle.Windsorコンテナーを使用してコントローラーを作成していることです。一時的なライフスタイルでコントローラーをマークできなかったため、各リクエストで同じインスタンスを取得していました。したがって、バインダーが使用しているコンテキストは再利用されており、もちろん古いデータが含まれていました。
Eilonのコードと私のコードの違いを注意深く分析している間に問題を発見し、他のすべての可能性を排除しました。 城のドキュメントにあるように、これは<!> quot ;ひどい間違い<!> quot ;!これを他の人に警告しましょう!
ご回答ありがとうございます。エイロン-お時間を割いて申し訳ありません。
他のヒント
この問題を再現しようとしましたが、同じ動作が見られません。私はあなたが持っているものとほぼ同じコントローラーとビューを(いくつかの仮定を付けて)作成し、毎回新しい<!> quot; RunTime <!> quot;を作成しました。その値をTempDataに入れて、リダイレクト経由で送信しました。その後、ターゲットページで値を取得しましたが、それは常にそのリクエストで入力した値でした。古い値ではありません。
ここに私のコントローラーがあります:
public class HomeController:Controller { public ActionResult Index(){ ViewData [<!> quot; Title <!> quot;] = <!> quot; Home Page <!> quot ;; string message = <!> quot;ようこそ:<!> quot; + TempData [<!> quot; Message <!> quot;]; if(TempData.ContainsKey(<!> quot; value <!> quot;)){ int theValue =(int)TempData [<!> quot; value <!> quot;]; メッセージ+ = <!> quot; <!> quot; + theValue.ToString(); } ViewData [<!> quot; Message <!> quot;] = message; return View(); }
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Create() {
return View(RunTime.Default);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(RunTime newRunTime) {
if (ModelState.IsValid) {
//service.CreateNewRun(newRunTime);
TempData["Message"] = "New run created";
TempData["value"] = newRunTime.TheValue;
return RedirectToAction("index");
}
return View(newRunTime);
}
}
そして、これが私のビュー(Create.aspx)です:
<% using (Html.BeginForm()) { %>
<%= Html.TextBox("newRunTime.TheValue", ViewData.Model.TheValue) %>
<input type="submit" value="Save" />
<% } %>
また、<!> quot; RunTime <!> quot;タイプは次のように見えたので、これを作成しました:
public class RunTime {
public static readonly RunTime Default = new RunTime(-1);
public RunTime() {
}
public RunTime(int theValue) {
TheValue = theValue;
}
public int TheValue {
get;
set;
}
}
RunTimeの実装に静的な値などが含まれている可能性はありますか?
ありがとう、
エイロン
これが関連しているかどうかはわかりませんが、
<!> lt;%= Html.TextBox(<!> quot; newRunTime.Time <!> quot ;, ViewData.Model.Time)%<!> gt;
実際には間違ったオーバーロードを選択する可能性があります(Timeは整数であるため、object htmlAttributes
ではなくstring value
オーバーロードを選択します。
レンダリングされたHTMLをチェックすると、これが発生しているかどうかがわかります。 intをViewData.Model.Time.ToString()
に変更すると、正しいオーバーロードが強制されます。
あなたの問題は何か違うようですが、私はそのことに気づき、過去に焼き付けられました。
セブ、あなたが例で何を意味するのか分かりません。 Unityの構成については何も知りません。 Castle.Windsorの状況について説明します。これは、Unityを正しく構成するのに役立つかもしれません。
デフォルトでは、Castle.Windsorは、指定されたタイプをリクエストするたびに同じオブジェクトを返します。これがシングルトンライフスタイルです。 Castle.Windsorのドキュメント<に、さまざまなライフスタイルオプションの適切な説明があります。
ASP.NET MVCでは、コントローラークラスの各インスタンスは、提供するために作成されたWeb要求のコンテキストにバインドされます。そのため、IoCコンテナーがコントローラークラスの同じインスタンスを毎回返す場合、そのコントローラークラスを使用した最初のWeb要求のコンテキストにバインドされたコントローラーを常に取得します。特に、ModelState
およびDefaultModelBinder
で使用される他のオブジェクトは再利用されるため、バインドされたモデルオブジェクトと<=>内の検証メッセージは古くなっています。
したがって、MVCがコントローラークラスのインスタンスを要求するたびに、IoCが新しいインスタンスを返す必要があります。
Castle.Windsorでは、これは一時的なライフスタイルと呼ばれます。設定するには、2つのオプションがあります:
- XML構成:lifestlye = <!> quot; transient <!> quot;を追加します。構成ファイル内のコントローラーを表す各要素に。
- コード内構成:コントローラーに登録するときに一時的なライフスタイルを使用するようにコンテナーに指示できます。これは、Benが言及したMvcContribヘルパーが自動的に行うことです- MvcContribソースコード。
Unityは、Castle.Windsorのライフスタイルに似た概念を提供するので、コントローラーの一時的なライフスタイルに相当するものを使用するようにUnityを構成する必要があります。 MvcContribには、 Unityのサポート-多分あなたはそこを見ることができます。
これがお役に立てば幸いです。
ASP.NET MVC アプリで Windsor IoC コンテナーを使用しようとしたときに同様の問題に遭遇したため、それを機能させるために同じ発見の旅を経験する必要がありました。ここでは、他の人に役立つ可能性のある詳細をいくつか紹介します。
これを使用するのが、Global.asax の初期設定です。
if (_container == null)
{
_container = new WindsorContainer("config/castle.config");
ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(Container));
}
そして、コントローラー インスタンスを要求されたときに WindsorControllerFactory を使用すると、次のことが行われます。
return (IController)_container.Resolve(controllerType);
Windsor はすべてのコントローラーを正しくリンクしていましたが、何らかの理由でパラメーターがフォームから関連するコントローラー アクションに渡されませんでした。代わりに、正しいアクションを呼び出していたにもかかわらず、それらはすべて null でした。
デフォルトではコンテナがシングルトンを返すようになっていますが、これは明らかにコントローラにとって悪いことであり、問題の原因です。
http://www.castleproject.org/monorail/documentation/trunk/integration/windsor.html
ただし、ドキュメントには、コントローラーのライフスタイルを一時的に変更できることが記載されていますが、構成ファイルを使用している場合に実際にその方法を説明していません。それは十分に簡単であることがわかります。
<component
id="home.controller"
type="DoYourStuff.Controllers.HomeController, DoYourStuff"
lifestyle="transient" />
コードを変更しなくても、期待どおりに動作するはずです (すなわち、コンテナの 1 つのインスタンスによって毎回提供される一意のコントローラー)。そうすれば、私が知っている良い子や女の子のようなコードではなく、構成ファイルですべての IoC 構成を行うことができます。