问题:如何在发布+验证方案中更新ModelState。
我有一个简单的表格:
<%= Html.ValidationSummary() %>
<% using(Html.BeginForm())%>
<%{ %>
<%=Html.TextBox("m.Value") %>
<input type="submit" />
<%} %>
当用户提交我想要验证输入时,在某些情况下我想为用户修复错误,让他知道他犯了一个已修复的错误:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(M m)
{
if (m.Value != "a")
{
ModelState.AddModelError("m.Value", "should be \"a\"");
m.Value = "a";
return View(m);
}
return View("About");
}
问题是,MVC将简单地忽略传递给视图的模型,并将重新呈现用户键入的内容 - 而不是我的值(“a”)。 发生这种情况,因为TextBox渲染器检查是否存在ModelState,如果它不为null,则使用ModelState的值。该值当然是在发布之前键入的一个用户。
由于我无法更改TextBox渲染器的行为,因此我找到的唯一解决方案是自行更新ModelState。 quick'n'dirty方法是(ab)使用DefaultModelBinder并覆盖通过简单地更改赋值方向将表单中的值分配给模型的方法;)。使用DefaultModelBinder我不必解析id。 以下代码(基于DefaultModelBinder的原始实现)是我的解决方案:
/// <summary>
/// Updates ModelState using values from <paramref name="order"/>
/// </summary>
/// <param name="order">Source</param>
/// <param name="prefix">Prefix used by Binder. Argument name in Action (if not explicitly specified).</param>
protected void UpdateModelState(object model, string prefix)
{
new ReversedBinder().BindModel(this.ControllerContext,
new ModelBindingContext()
{
Model = model,
ModelName = prefix,
ModelState = ModelState,
ModelType = model.GetType(),
ValueProvider = ValueProvider
});
}
private class ReversedBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
string prefix = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
object val = typeof(Controller)
.Assembly.GetType("System.Web.Mvc.DictionaryHelpers")
.GetMethod("DoesAnyKeyHavePrefix")
.MakeGenericMethod(typeof(ValueProviderResult))
.Invoke(null, new object[] { bindingContext.ValueProvider, prefix });
bool res = (bool)val;
if (res)
{
IModelBinder binder = new ReversedBinder();//this.Binders.GetBinder(propertyDescriptor.PropertyType);
object obj2 = propertyDescriptor.GetValue(bindingContext.Model);
ModelBindingContext context2 = new ModelBindingContext();
context2.Model = obj2;
context2.ModelName = prefix;
context2.ModelState = bindingContext.ModelState;
context2.ModelType = propertyDescriptor.PropertyType;
context2.ValueProvider = bindingContext.ValueProvider;
ModelBindingContext context = context2;
object obj3 = binder.BindModel(controllerContext, context);
if (bindingContext.ModelState.Keys.Contains<string>(prefix))
{
var prefixKey = bindingContext.ModelState.Keys.First<string>(x => x == prefix);
bindingContext.ModelState[prefixKey].Value
= new ValueProviderResult(obj2, obj2.ToString(),
bindingContext.ModelState[prefixKey].Value.Culture);
}
}
}
}
所以问题仍然存在:我做的事情是非常不寻常的还是我错过了什么?如果是前者,那么我怎样才能以更好的方式实现这些功能(使用现有的MVC基础设施)?
答案 0 :(得分:23)
我知道这篇文章相当陈旧,但这是我之前遇到的一个问题,我只是想到了一个我喜欢的简单解决方案 - 只需在获得发布值后清除ModelState。
UpdateModel(viewModel);
ModelState.Clear();
viewModel.SomeProperty = "a new value";
return View(viewModel);
并且视图必须使用(可能已修改的)视图模型对象而不是ModelState。
也许这很明显。事后看来似乎如此!
答案 1 :(得分:4)
您可以接受表单集合作为参数而不是控制器中的模型对象,如下所示:public ActionResult Index(FormCollection Form)
。
因此,默认模型绑定器不会更新模型状态,您将获得所需的行为。
修改:或者您可以更新ModelStateDictionary以反映您对模型所做的更改。
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(M m)
{
if (m.Value != "a")
{
ModelState["m.Value"].Value = new ValueProviderResult("a", m.Name,
CultureInfo.CurrentCulture);
ModelState.AddModelError("m.Value", "should be \"a\"");
m.Value = "a";
return View(m);
}
return View("About");
}
注意:我不确定这是不是最好的方法。但它似乎有效,它应该是你想要的行为。
答案 2 :(得分:0)
我正在做一些非常罕见的事情,或者我错过了什么?
我认为这种情况非常罕见。我认为MVC假设验证错误是肯定/否定事件,在这种情况下,您将使用验证错误作为提供一般用户反馈的手段。
我认为当POST由于验证错误而失败,或执行操作并重定向或呈现完全不同的内容时,MVC似乎也最开心。在模型验证错误之外,重新渲染相同的输入非常罕见。
我已经使用MVC大约一年了,只是在另一个上下文中遇到了这个问题,在POST后我想渲染一个新的表单作为响应。
[HttpPost]
public ActionResult Upload(DocumentView data) {
if(!ModelState.IsValid) return View(data);
ProcessUpload(data);
return View(new DocumentView());
}
MVC正在从ModelState
呈现data
,而不是我的新对象。非常令人惊讶。
如果是前者,那我怎么能以更好的方式实现这样的功能