ASP.NET MVC Beta 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
<!> gt;)包含如下指令:
<%= Html.TextBox("newRunTime.Time", ViewData.Model.Time) %>
这使用DefaultModelBinder
类,意味着自动绑定我的模型的属性。
我点击页面,输入有效数据(例如时间= 1)。应用程序正确保存新对象时间= 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);
}
}
我错过了什么吗?我不认为这是一个浏览器会话问题,因为如果第一个数据在一个浏览器中输入而第二个数据在另一个浏览器中输入,我可以重现该问题。
解决方案
事实证明问题是我的控制器在呼叫之间被重用。我选择从原始帖子中省略的一个细节是我使用Castle.Windsor容器来创建我的控制器。我没有使用Transient生活方式标记我的控制器,所以我在每个请求上都得到了相同的实例。因此,粘合剂使用的上下文正在被重用,当然它包含陈旧的数据。
我在仔细分析Eilon的代码和我的代码之间的区别时发现了这个问题,消除了所有其他可能性。正如 Castle文档所说,这是<!> ;可怕的错误<!>!让这成为对他人的警告!
感谢您回复Eilon - 抱歉占用您的时间。
其他提示
我试图重现这个问题,但我没有看到同样的行为。我创建了几乎完全相同的控制器和视图(有一些假设),每次我创建一个新的<!>“RunTime <!>”;我将其值放在TempData中并通过Redirect将其发送出去。然后在目标页面上我抓住了值,它始终是我在该请求中键入的值 - 从不是陈旧的值。
这是我的控制器:
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" />
<% } %>
另外,我不确定<!>“RunTime <!>”是什么类型看起来像,所以我做了这个:
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实现是否可能包含一些静态值?
谢谢,
Eilon
我不确定这是否相关,但是你打电话给我
<!> lt;%= Html.TextBox(<!> quot; newRunTime.Time <!> quot;,ViewData.Model.Time)%<!> gt;
可能实际上选择了错误的重载(因为Time是一个整数,它会选择object htmlAttributes
重载,而不是string value
。
检查呈现的HTML会告诉您这是否正在发生。将int更改为ViewData.Model.Time.ToString()
将强制正确的重载。
听起来你的问题有所不同,但我注意到了这一点并且过去曾被烧毁过。
Seb,我不确定你的例子是什么意思。我对Unity配置一无所知。我将用Castle.Windsor解释这种情况,这可能会帮助你正确配置Unity。
默认情况下,每次请求给定类型时,Castle.Windsor都会返回相同的对象。这是单身人士的生活方式。有关 Castle.Windsor文档<中各种生活方式选项的详细解释/ A>。
在ASP.NET MVC中,控制器类的每个实例都绑定到创建它以供服务的Web请求的上下文。因此,如果您的IoC容器每次都返回相同的控制器类实例,那么您将始终将控制器绑定到使用该控制器类的第一个Web请求的上下文。特别是,ModelState
使用的DefaultModelBinder
和其他对象将被重用,因此绑定的模型对象和<=>中的验证消息将是陈旧的。
因此,每次MVC请求控制器类的实例时,您都需要IoC返回一个新实例。
在Castle.Windsor中,这被称为短暂的生活方式。要进行配置,您有两种选择:
- 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正确地连接了所有控制器,但由于某种原因,参数没有从表单传递到相关的控制器操作。相反,他们都是空的,虽然它正在调用正确的行动。
默认情况下,容器会传回单例,这显然是控制器的坏事和问题的原因:
http://www.castleproject.org/monorail/documentation /trunk/integration/windsor.html
然而,文档确实指出控制器的生活方式可以更改为瞬态,但如果您使用配置文件,它实际上并没有告诉您如何执行此操作。事实证明这很容易:
<component
id="home.controller"
type="DoYourStuff.Controllers.HomeController, DoYourStuff"
lifestyle="transient" />
如果没有任何代码更改,它现在应该按预期工作(即每次由容器的一个实例提供的唯一控制器)。然后,您可以在配置文件中执行所有IoC配置,而不是像我认识的那样好的男孩/女孩。