Il problema: come aggiornare ModelState nello scenario di posting + validation.Aggiornamento di ModelState con l'oggetto modello
Ho una forma semplice:
<%= Html.ValidationSummary() %>
<% using(Html.BeginForm())%>
<%{ %>
<%=Html.TextBox("m.Value") %>
<input type="submit" />
<%} %>
Quando l'utente invia voglio per convalidare l'input e, in alcune circostanze voglio correggere l'errore per l'utente, fargli sapere che ha fatto un errore che è già fissato:
[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");
}
Ebbene il problema è, MVC semplicemente ignorerà il modello passato alla vista e si ri-renderizzare tutto ciò che l'utente ha digitato - e non il mio valore ("a"). Questo accade, perché il renderer TextBox controlla se esiste un ModelState e se non è null - viene utilizzato il valore di ModelState. Questo valore è ovviamente l'unico utente digitato prima della pubblicazione.
Dal momento che non riesco a modificare il comportamento del renderer di TextBox, l'unica soluzione che ho trovato sarebbe stata aggiornare il ModelState da solo. Il modo quick'n'dirty è di (ab) utilizzare DefaultModelBinder e sovrascrivere il metodo che assegna i valori dalle forme al modello semplicemente cambiando la direzione dell'assegnazione;). Utilizzando DefaultModelBinder non devo analizzare gli id. Il seguente codice (sulla base di implementazione originale di DefaultModelBinder) è la mia soluzione a questo:
/// <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);
}
}
}
}
Quindi la domanda rimane: sto facendo qualcosa di estremamente raro o mi sto perdendo qualcosa? Se il primo, allora come potrei implementare tale funzionalità in un modo migliore (usando l'infrastruttura MVC esistente)?
Ma voglio ottenere il collegamento predefinito. Lo voglio perché voglio usare ModelState. Voglio solo aggiornare il ModelState per riflettere i cambiamenti nel mio oggetto modello. – user87338
Vedi la mia modifica. –
Il tuo commento è esattamente quello che sto facendo, ma tu lo fai da solo e sto usando il deault binder quindi ho qualcosa di più "generico". Il cambio di valore ("a") si verifica in un livello inferiore, quindi non conosco realmente quali oggetti sono stati modificati. E inoltre non si desidera ModelState ["m.Value"]. Value = new ValueProviderResult ("a", m.Name, CultureInfo.CurrentCulture); per la proprietà di ogni singolo oggetto, si :). – user87338