2009-04-05 11 views
12

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)?

risposta

4

È possibile accettare una raccolta moduli come parametro anziché come oggetto modello nel controller, ad esempio: public ActionResult Index(FormCollection Form).

Pertanto, il raccoglitore modello predefinito non aggiornerà lo stato del modello e otterrete il comportamento desiderato.

Modifica: Oppure è possibile aggiornare semplicemente ModelStateDictionary per riflettere le modifiche apportate al modello.


[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");    
} 

Nota: non sono sicuro se questo è il modo migliore. Ma sembra funzionare e dovrebbe essere il comportamento che desideri.

+0

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

+0

Vedi la mia modifica. –

+0

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

22

So che questo post è abbastanza vecchio, ma è un problema che ho avuto prima e ho appena pensato a una soluzione semplice che mi piace - basta cancellare il ModelState dopo aver ottenuto i valori pubblicati.

UpdateModel(viewModel); 
ModelState.Clear(); 

viewModel.SomeProperty = "a new value"; 
return View(viewModel); 

e la vista deve utilizzare l'oggetto del modello di visualizzazione (eventualmente modificato) anziché il ModelState.

Forse questo è davvero ovvio. Sembra così a ben vedere!

0

sto facendo qualcosa di estremamente insolito o mi manca qualcosa?

Penso che sia piuttosto raro. Penso che MVC stia presupponendo che gli errori di validazione siano sì/no, e in questo caso stai utilizzando un errore di convalida come mezzo per fornire un feedback generale agli utenti.

Penso che MVC sembri anche più felice quando i POST falliscono a causa di errori di validazione, o eseguono un'azione e reindirizzano o rendono qualcosa di completamente diverso. Al di fuori degli errori di validazione del modello, è piuttosto raro eseguire nuovamente il rendering dello stesso input.

Sto usando MVC da circa un anno e mi sono imbattuto in questo in un altro contesto, dove dopo un POST volevo rendere una nuova forma come risposta.

[HttpPost] 
public ActionResult Upload(DocumentView data) { 
    if(!ModelState.IsValid) return View(data); 
    ProcessUpload(data); 
    return View(new DocumentView()); 
} 

MVC sta rendendo il ModelState da data, non il mio nuovo oggetto. Molto sorprendente.

Nel primo caso, quindi come potrei implementare tale funzionalità in un modo migliore

  1. attrezzo per correzioni automatiche in javascript (potrebbe non essere possibile)
  2. tenere un elenco di correzioni automatiche fatto, se l'oggetto è valido dopo tutti quelli poi passarlo alla vista "Informazioni" e visualizzare come un messaggio come "M salvato, con le seguenti correzioni: ...".
Problemi correlati