ASP.NET MVC Beta 1: DefaultModelBinder persiste erroneamente il parametro e lo stato di convalida tra richieste non correlate

StackOverflow https://stackoverflow.com/questions/238460

Domanda

Quando utilizzo l'associazione del modello predefinito per associare i parametri del modulo a un oggetto complesso che è un parametro di un'azione, il framework ricorda i valori passati alla prima richiesta, il che significa che qualsiasi richiesta successiva a quell'azione ottiene gli stessi dati di il primo. I valori dei parametri e lo stato di convalida vengono mantenuti tra richieste Web non correlate.

Ecco il mio codice controller (service rappresenta l'accesso al back-end dell'app):

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

La mia vista .aspx (fortemente tipizzata come ViewPage<RunTime >) contiene direttive come:

<%= Html.TextBox("newRunTime.Time", ViewData.Model.Time) %>

Utilizza la classe DefaultModelBinder, che è significavano legare automaticamente le proprietà del mio modello .

Ho colpito la pagina, ho inserito dati validi (ad es. tempo = 1). L'app salva correttamente il nuovo oggetto con time = 1. Poi lo premo di nuovo, inserisco diversi dati validi (ad es. Time = 2). Tuttavia, i dati che vengono salvati sono l'originale (ad es. Tempo = 1). Ciò influisce anche sulla convalida, quindi se i miei dati originali non erano validi, tutti i dati che inserirò in futuro saranno considerati non validi. Il riavvio di IIS o la ricostruzione del mio codice cancella lo stato persistente.

Posso risolvere il problema scrivendo il mio modello di rilegatore hardcoded, un esempio ingenuo di base che viene mostrato di seguito.

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

Mi sto perdendo qualcosa? Non penso che sia un problema di sessione del browser in quanto posso riprodurre il problema se i primi dati vengono immessi in un browser e il secondo in un altro.

È stato utile?

Soluzione

Si scopre che il problema era che i miei controller venivano riutilizzati tra le chiamate. Uno dei dettagli che ho scelto di omettere dal mio post originale è che sto usando il contenitore Castle.Windsor per creare i miei controller. Non ero riuscito a contrassegnare il mio controller con lo stile di vita Transient, quindi stavo ottenendo la stessa istanza su ogni richiesta. Pertanto, il contesto utilizzato dal raccoglitore veniva riutilizzato e ovviamente conteneva dati non aggiornati.

Ho scoperto il problema analizzando attentamente la differenza tra il codice di Eilon e il mio, eliminando tutte le altre possibilità. Come dice la la documentazione del castello , questa è una & Quot ; terribile errore " ;! Lascia che questo sia un avvertimento per gli altri!

Grazie per la tua risposta Eilon - scusa il tempo che hai dedicato.

Altri suggerimenti

Ho provato a riprodurre questo problema ma non vedo lo stesso comportamento. Ho creato quasi esattamente lo stesso controller e le stesse viste che hai (con alcuni presupposti) e ogni volta che ho creato un nuovo & Quot; RunTime & Quot; Ho messo il suo valore in TempData e l'ho inviato tramite il reindirizzamento. Quindi nella pagina di destinazione ho preso il valore ed era sempre il valore che ho digitato su quella richiesta - mai un valore stantio.

Ecco il mio controller:

HomeController di classe pubblica: Controller {     indice pubblico ActionResult () {         ViewData [& Quot; Titolo & Quot;] = & Quot; Home Page & Quot ;;         string message = " Benvenuto: " + TempData [& Quot; Messaggio & Quot;];         if (TempData.ContainsKey (" valore ")) {             int theValue = (int) TempData [" valore "];             messaggio + = " & Quot; + theValue.ToString ();         }         ViewData [& Quot; Messaggio & Quot;] = messaggio;         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);
}

}

Ed ecco la mia vista (Create.aspx):

<% using (Html.BeginForm()) { %>
<%= Html.TextBox("newRunTime.TheValue", ViewData.Model.TheValue) %>
<input type="submit" value="Save" />
<% } %>

Inoltre, non ero sicuro di quale " RunTime " sembrava tipo, quindi ho creato questo:

   public class RunTime {
        public static readonly RunTime Default = new RunTime(-1);

        public RunTime() {
        }

        public RunTime(int theValue) {
            TheValue = theValue;
        }

        public int TheValue {
            get;
            set;
        }
    }

È possibile che l'implementazione di RunTime includa alcuni valori statici o qualcosa del genere?

Grazie,

Eilon

Non sono sicuro che questo sia correlato o meno, ma la tua chiamata a <% = Html.TextBox (" newRunTime.Time " ;, ViewData.Model.Time)% > potrebbe effettivamente scegliere il sovraccarico errato (poiché Time è un numero intero, selezionerà il object htmlAttributes sovraccarico, anziché string value.

Il controllo dell'HTML renderizzato ti farà sapere se si sta verificando. la modifica di int in ViewData.Model.Time.ToString() forza il sovraccarico corretto.

Sembra che il tuo problema sia qualcosa di diverso, ma l'ho notato e sono stato bruciato in passato.

Seb, non sono sicuro di cosa intendi con un esempio. Non so nulla sulla configurazione di Unity. Spiegherò la situazione con Castle.Windsor e forse questo ti aiuterà a configurare Unity correttamente.

Per impostazione predefinita, Castle.Windsor restituisce lo stesso oggetto ogni volta che si richiede un determinato tipo. Questo è lo stile di vita singleton. C'è una buona spiegazione delle varie opzioni di stile di vita nella Documentazione Castle.Windsor .

In ASP.NET MVC, ogni istanza di una classe di controller è legata al contesto della richiesta Web che è stata creata per servire. Pertanto, se il contenitore IoC restituisce ogni volta la stessa istanza della classe del controller, si otterrà sempre un controller associato al contesto della prima richiesta Web che ha utilizzato quella classe del controller. In particolare, ModelState e gli altri oggetti utilizzati da DefaultModelBinder verranno riutilizzati, pertanto l'oggetto modello associato e i messaggi di convalida in <=> saranno obsoleti.

Pertanto è necessario che l'IoC restituisca una nuova istanza ogni volta che MVC richiede un'istanza della classe del controller.

In Castle.Windsor, questo è chiamato stile di vita transitorio. Per configurarlo, hai due opzioni:

  1. Configurazione XML: aggiungi lifestlye = " transient " a ciascun elemento nel file di configurazione che rappresenta un controller.
  2. Configurazione in-code: puoi dire al contenitore di usare lo stile di vita transitorio nel momento in cui registri il controller. Questo è ciò che l'helper MvcContrib che Ben ha menzionato fa automaticamente per te: dai un'occhiata al metodo RegisterControllers in codice sorgente MvcContrib .

Immagino che Unity offra un concetto simile allo stile di vita in Castle.Windsor, quindi dovrai configurare Unity per usare il suo equivalente dello stile di vita transitorio per i tuoi controller. Sembra che MvcContrib abbia Supporto di unità - forse potresti guardare lì.

Spero che questo aiuti.

Avendo riscontrato problemi simili durante il tentativo di utilizzare il contenitore Windsor IoC in un'app ASP.NET MVC, ho dovuto affrontare lo stesso viaggio di scoperta per farlo funzionare. Ecco alcuni dei dettagli che potrebbero aiutare qualcun altro.

L'utilizzo di questa è la configurazione iniziale in Global.asax:

  if (_container == null) 
  {
    _container = new WindsorContainer("config/castle.config");
    ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(Container)); 
  }

E usando un WindsorControllerFactory che quando richiesto per un'istanza del controller fa:

  return (IController)_container.Resolve(controllerType);

Mentre Windsor collegava correttamente tutti i controller, per qualche motivo i parametri non venivano passati dal modulo all'azione del controller pertinente. Invece erano tutti nulli, sebbene stesse chiamando l'azione corretta.

L'impostazione predefinita è che il contenitore restituisca singleton, ovviamente una cosa negativa per i controller e la causa del problema:

http://www.castleproject.org/monorail/documentation /trunk/integration/windsor.html

Tuttavia, la documentazione sottolinea che lo stile di vita dei controller può essere modificato in transitorio, anche se in realtà non ti dice come farlo se si utilizza un file di configurazione. Si scopre che è abbastanza facile:

<component 
  id="home.controller" 
  type="DoYourStuff.Controllers.HomeController, DoYourStuff" 
  lifestyle="transient" />

E senza alcuna modifica del codice, ora dovrebbe funzionare come previsto (ovvero controller univoci ogni volta forniti da un'istanza del contenitore). Puoi quindi eseguire tutta la tua configurazione IoC nel file di configurazione anziché nel codice come il bravo ragazzo / ragazza che so che sei.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top