Asp.net MVC ModelState.Clear


116

Qualcuno può darmi una definizione succinta del ruolo di ModelState in Asp.net MVC (o un collegamento a uno). In particolare ho bisogno di sapere in quali situazioni è necessario o auspicabile chiamare ModelState.Clear().

Un po 'aperto eh ... scusa, penso che potrebbe essere d'aiuto se ti dicessi cosa sto facendo:

Ho un'azione di modifica su un controller chiamato "Pagina". Quando vedo per la prima volta il modulo per modificare i dettagli della pagina, tutto viene caricato correttamente (associazione a un oggetto "MyCmsPage"). Quindi faccio clic su un pulsante che genera un valore per uno dei campi dell'oggetto MyCmsPage ( MyCmsPage.SeoTitle). Genera bene e aggiorna l'oggetto e quindi restituisco il risultato dell'azione con l'oggetto della pagina appena modificato e mi aspetto che la casella di testo pertinente (renderizzata usando <%= Html.TextBox("seoTitle", page.SeoTitle)%>) venga aggiornata ... ma ahimè mostra il valore dal vecchio modello che è stato caricato.

Ho risolto il problema usando ModelState.Clear()ma ho bisogno di sapere perché / come ha funzionato, quindi non lo sto facendo alla cieca.

PageController:

[AcceptVerbs("POST")]
public ActionResult Edit(MyCmsPage page, string submitButton)
{
    // add the seoTitle to the current page object
    page.GenerateSeoTitle();

    // why must I do this?
    ModelState.Clear();

    // return the modified page object
     return View(page);
 }

aspx:

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyCmsPage>" %>
....
        <div class="c">
            <label for="seoTitle">
                Seo Title</label>
            <%= Html.TextBox("seoTitle", page.SeoTitle)%>
            <input type="submit" value="Generate Seo Title" name="submitButton" />
        </div>

Noob AspMVC, se vuole memorizzare nella cache i vecchi dati, allora qual è il punto nel dare di nuovo il modello all'utente: @ ho avuto lo stesso problema, grazie mille fratello
deadManN

Risposte:


135

Penso che sia un bug in MVC. Oggi ho lottato con questo problema per ore.

Dato ciò:

public ViewResult SomeAction(SomeModel model) 
{
    model.SomeString = "some value";
    return View(model); 
}

La vista viene renderizzata con il modello originale, ignorando le modifiche. Quindi ho pensato, forse non mi piace usare lo stesso modello, quindi ho provato in questo modo:

public ViewResult SomeAction(SomeModel model) 
{
    var newModel = new SomeModel { SomeString = "some value" };
    return View(newModel); 
}

E ancora la vista viene renderizzata con il modello originale. La cosa strana è che quando metto un punto di interruzione nella vista ed esamino il modello, ha il valore modificato. Ma il flusso di risposta ha i vecchi valori.

Alla fine ho scoperto lo stesso lavoro che hai fatto tu:

public ViewResult SomeAction(SomeModel model) 
{
    var newModel = new SomeModel { SomeString = "some value" };
    ModelState.Clear();
    return View(newModel); 
}

Funziona come previsto.

Non penso che questa sia una "caratteristica", vero?


33
Ho appena fatto quasi la stessa cosa che hai fatto tu. Ho scoperto che questo non è un bug, però. È di progettazione: un bug? EditorFor e DisplayFor non visualizzano lo stesso valore e gli helper HTML di ASP.NET MVC rendono il valore sbagliato
Metro Smurf

8
Amico, ho già passato 2 ore a combattere con esso. Grazie per aver postato questa risposta!
Andrey Agibalov

37
questo è ancora vero e molte persone, me compreso, stanno perdendo molto tempo a causa di questo. bug o di progettazione, non mi interessa, è "inaspettato".
Proviste il

7
Sono d'accordo con @Proviste, spero che questa "funzione" venga rimossa in futuro
Ben

8
Ci ho appena passato quattro ore. Brutta.
Brian MacKay

46

Aggiornare:

  • Questo non è un bug.
  • Smetti di tornare View()da un'azione POST. Utilizza invece PRG e reindirizza a un GET se l'azione ha successo.
  • Se si sta ritornando una View()da un'azione POST, farlo per la validazione dei form, e farlo nel modo MVC è stato progettato utilizzando il costruito nel aiutanti. Se lo fai in questo modo, non dovresti aver bisogno di usare.Clear()
  • Se stai usando questa azione per restituire ajax per una SPA , usa un controller web api e dimenticalo ModelStatedato che non dovresti usarlo comunque.

Vecchia risposta:

ModelState in MVC viene utilizzato principalmente per descrivere lo stato di un oggetto del modello in gran parte in relazione al fatto che l'oggetto sia valido o meno. Questo tutorial dovrebbe spiegare molto.

In genere non dovrebbe essere necessario cancellare ModelState poiché viene gestito dal motore MVC. La cancellazione manuale potrebbe causare risultati indesiderati quando si tenta di aderire alle migliori pratiche di convalida MVC.

Sembra che tu stia cercando di impostare un valore predefinito per il titolo. Questo dovrebbe essere fatto quando l'oggetto del modello viene istanziato (livello di dominio da qualche parte o nell'oggetto stesso - ctor senza parametri), sull'azione get in modo tale che scenda alla pagina la prima volta o completamente sul client (tramite ajax o qualcosa del genere) in modo che sembri come se l'utente lo avesse inserito e ritorni con la raccolta di moduli pubblicati. In qualche modo il tuo approccio di aggiungere questo valore alla ricezione di una raccolta di moduli (nell'azione POST // Modifica) sta causando questo comportamento bizzarro che potrebbe far .Clear() sembrare che funzioni per te. Credimi, non vuoi usare il clear. Prova una delle altre idee.


1
Mi aiuta a ripensare un po 'il mio livello di servizi (geme ma grazie) ma come con molte cose in rete si appoggia pesantemente al punto di vista dell'utilizzo di ModelState per la convalida.
Mr Grok

Aggiunte ulteriori informazioni alla domanda per mostrare perché sono particolarmente interessato a ModelState.Clear () e il motivo della mia domanda
Mr Grok

5
Non acquisto questo argomento per smettere di restituire View (...) da una funzione [HttpPost]. Se stai POST del contenuto tramite ajax e quindi aggiorni il documento con il PartialView risultante, è stato dimostrato che MVC ModelState non è corretto. L'unica soluzione alternativa che ho trovato è cancellarlo nel metodo del controller.
Aaron Hudon

@AaronHudon PRG è abbastanza ben consolidato.
Matt Kocaj

Se effettuo il POST con una chiamata AJAX, posso reindirizzare a un'azione GET e restituire una visualizzazione riempita di modello come vuole l'OP, tutto in modo asincrono?
MyiEye

17

Se vuoi cancellare un valore per un singolo campo, ho trovato utile la seguente tecnica.

ModelState.SetModelValue("Key", new ValueProviderResult(null, string.Empty, CultureInfo.InvariantCulture));

Nota: cambia "Chiave" con il nome del campo che desideri reimpostare.


Non so perché questo ha funzionato in modo diverso per me (forse MVC4)? Ma ho dovuto fare anche model.Key = "" in seguito. Entrambe le linee sono obbligatorie.
TTT

Vorrei farti i complimenti per la rimozione del commento @PeterGluck. È meglio che cancellare lo stato del modello completo (poiché ho errori su alcuni campi che vorrei mantenere).
Tjab

6

Bene, ModelState mantiene fondamentalmente lo stato corrente del modello in termini di convalida, vale

ModelErrorCollection: rappresenta gli errori quando il modello tenta di associare i valori. ex.

TryUpdateModel();
UpdateModel();

o come un parametro in ActionResult

public ActionResult Create(Person person)

ValueProviderResult : conserva i dettagli sul tentativo di associazione al modello. ex. AttemptedValue, Culture, RawValue .

Il metodo Clear () deve essere utilizzato con cautela perché può portare a risultati imprevisti. E perderai alcune belle proprietà di ModelState come AttemptedValue, questo viene utilizzato da MVC in background per ripopolare i valori del modulo in caso di errore.

ModelState["a"].Value.AttemptedValue

1
hmmm ... Questo potrebbe essere il punto in cui sto riscontrando il problema a quanto pare. Ho controllato il valore della proprietà Model.SeoTitle ed è cambiato ma il valore tentato non lo è. Sembra che stia mantenendo il valore come se ci fosse un errore nella pagina anche se non ce n'è uno (ho controllato il dizionario ModelState e non ci sono errori).
Mr Grok

6

Ho avuto un'istanza in cui volevo aggiornare il modello di un modulo indicato e non volevo "Reindirizzare all'azione" per motivi di prestazioni. I valori precedenti dei campi nascosti venivano mantenuti sul mio modello aggiornato, causando problemi di ogni tipo !.

Poche righe di codice hanno presto identificato gli elementi all'interno di ModelState che volevo rimuovere (dopo la convalida), quindi i nuovi valori sono stati utilizzati nella forma: -

while (ModelState.FirstOrDefault(ms => ms.Key.ToString().StartsWith("SearchResult")).Value != null)
{
    ModelState.Remove(ModelState.FirstOrDefault(ms => ms.Key.ToString().StartsWith("SearchResult")));
}

5

Beh, molti di noi sembrano essere stati morsi da questo, e anche se il motivo per cui ciò accade ha senso, avevo bisogno di un modo per assicurarmi che il valore sul mio modello fosse mostrato e non ModelState.

Alcuni hanno suggerito ModelState.Remove(string key), ma non è ovvio cosa keydovrebbe essere, soprattutto per i modelli annidati. Ecco un paio di metodi che ho escogitato per aiutarti.

Il RemoveStateFormetodo prenderà un ModelStateDictionary, un modello e un'espressione per la proprietà desiderata e la rimuoverà. HiddenForModelpuò essere utilizzato nella vista per creare un campo di input nascosto utilizzando solo il valore dal modello, rimuovendo prima la sua voce ModelState. (Questo potrebbe essere facilmente espanso per gli altri metodi di estensione helper).

/// <summary>
/// Returns a hidden input field for the specified property. The corresponding value will first be removed from
/// the ModelState to ensure that the current Model value is shown.
/// </summary>
public static MvcHtmlString HiddenForModel<TModel, TProperty>(this HtmlHelper<TModel> helper,
    Expression<Func<TModel, TProperty>> expression)
{
    RemoveStateFor(helper.ViewData.ModelState, helper.ViewData.Model, expression);
    return helper.HiddenFor(expression);
}

/// <summary>
/// Removes the ModelState entry corresponding to the specified property on the model. Call this when changing
/// Model values on the server after a postback, to prevent ModelState entries from taking precedence.
/// </summary>
public static void RemoveStateFor<TModel, TProperty>(this ModelStateDictionary modelState, TModel model,
    Expression<Func<TModel, TProperty>> expression)
{
    var key = ExpressionHelper.GetExpressionText(expression);

    modelState.Remove(key);
}

Chiama da un controller come questo:

ModelState.RemoveStateFor(model, m => m.MySubProperty.MySubValue);

o da una vista come questa:

@Html.HiddenForModel(m => m.MySubProperty.MySubValue)

Viene utilizzato System.Web.Mvc.ExpressionHelperper ottenere il nome della proprietà ModelState.


1
Molto bella! Tenere una scheda su questo per la funzionalità ExpressionHelper.
Gerard ONeill

4

Volevo aggiornare o reimpostare un valore se non è stato completamente convalidato e ho riscontrato questo problema.

La risposta facile, ModelState.Remove, è .. problematica .. perché se stai usando gli helper non conosci veramente il nome (a meno che non ti attenga alla convenzione di denominazione). A meno che tu non crei una funzione che sia il tuo assistente personalizzato che il tuo controller possono utilizzare per ottenere un nome.

Questa funzione avrebbe dovuto essere implementata come opzione sull'helper, dove per impostazione predefinita è non lo fa, ma se si desidera che l'input non accettato venga visualizzato di nuovo, è possibile dirlo.

Ma almeno ora capisco il problema;).


Avevo bisogno di fare esattamente questo; vedere i miei metodi che ho pubblicato di seguito che mi hanno aiutato a trovare Remove()la chiave corretta.
Tobias J

0

Alla fine ce l'ho fatta. Il mio ModelBinder personalizzato che non era stato registrato e fa questo:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

myCmsPage.SeoTitle = (!String.IsNullOrEmpty(frm["seoTitle"])) ? frm["seoTitle"] : null;

Quindi qualcosa che stava facendo l'associazione del modello predefinito deve aver causato il problema. Non sono sicuro di cosa, ma il mio problema è almeno risolto ora che il mio raccoglitore di modelli personalizzato viene registrato.


Beh, non ho esperienza con un ModelBinder personalizzato, quello predefinito si adatta alle mie esigenze finora =).
JOBG

0

In generale, quando ti ritrovi a combattere contro le pratiche standard di un framework, è tempo di riconsiderare il tuo approccio. In questo caso, il comportamento di ModelState. Ad esempio, se non vuoi lo stato del modello dopo un POST, considera un reindirizzamento al file get.

[HttpPost]
public ActionResult Edit(MyCmsPage page, string submitButton)
{
    if (ModelState.IsValid) {
        SomeRepository.SaveChanges(page);
        return RedirectToAction("GenerateSeoTitle",new { page.Id });
    }
    return View(page);
}

public ActionResult GenerateSeoTitle(int id) {
     var page = SomeRepository.Find(id);
     page.GenerateSeoTitle();
     return View("Edit",page);
}

MODIFICATO per rispondere al commento sulla cultura:

Ecco cosa utilizzo per gestire un'applicazione MVC multiculturale. Prima le sottoclassi del gestore di rotte:

public class SingleCultureMvcRouteHandler : MvcRouteHandler {
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        var culture = requestContext.RouteData.Values["culture"].ToString();
        if (string.IsNullOrWhiteSpace(culture))
        {
            culture = "en";
        }
        var ci = new CultureInfo(culture);
        Thread.CurrentThread.CurrentUICulture = ci;
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
        return base.GetHttpHandler(requestContext);
    }
}

public class MultiCultureMvcRouteHandler : MvcRouteHandler
{
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        var culture = requestContext.RouteData.Values["culture"].ToString();
        if (string.IsNullOrWhiteSpace(culture))
        {
            culture = "en";
        }
        var ci = new CultureInfo(culture);
        Thread.CurrentThread.CurrentUICulture = ci;
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
        return base.GetHttpHandler(requestContext);
    }
}

public class CultureConstraint : IRouteConstraint
{
    private string[] _values;
    public CultureConstraint(params string[] values)
    {
        this._values = values;
    }

    public bool Match(HttpContextBase httpContext,Route route,string parameterName,
                        RouteValueDictionary values, RouteDirection routeDirection)
    {

        // Get the value called "parameterName" from the 
        // RouteValueDictionary called "value"
        string value = values[parameterName].ToString();
        // Return true is the list of allowed values contains 
        // this value.
        return _values.Contains(value);

    }

}

public enum Culture
{
    es = 2,
    en = 1
}

Ed ecco come collego i percorsi. Dopo aver creato i percorsi, aggiungo il mio subagente (example.com/subagent1, example.com/subagent2, ecc.) All'inizio del codice della cultura. Se tutto ciò di cui hai bisogno è la cultura, rimuovi semplicemente l'agente secondario dai gestori di rotte e dalle rotte.

    public static void RegisterRoutes(RouteCollection routes)
    {

        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.IgnoreRoute("Content/{*pathInfo}");
        routes.IgnoreRoute("Cache/{*pathInfo}");
        routes.IgnoreRoute("Scripts/{pathInfo}.js");
        routes.IgnoreRoute("favicon.ico");
        routes.IgnoreRoute("apple-touch-icon.png");
        routes.IgnoreRoute("apple-touch-icon-precomposed.png");

        /* Dynamically generated robots.txt */
        routes.MapRoute(
            "Robots.txt", "robots.txt",
            new { controller = "Robots", action = "Index", id = UrlParameter.Optional }
        );

        routes.MapRoute(
             "Sitemap", // Route name
             "{subagent}/sitemap.xml", // URL with parameters
             new { subagent = "aq", controller = "Default", action = "Sitemap"},  new[] { "aq3.Controllers" } // Parameter defaults
        );

        routes.MapRoute(
             "Rss Feed", // Route name
             "{subagent}/rss", // URL with parameters
             new { subagent = "aq", controller = "Default", action = "RSS"},  new[] { "aq3.Controllers" } // Parameter defaults
        );

        /* remap wordpress tags to mvc blog posts */
        routes.MapRoute(
            "Tag", "tag/{title}",
            new { subagent = "aq", controller = "Default", action = "ThreeOhOne", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler(); ;

        routes.MapRoute(
            "Custom Errors", "Error/{*errorType}",
            new { controller = "Error", action = "Index", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        );

        /* dynamic images not loaded from content folder */
        routes.MapRoute(
            "Stock Images",
            "{subagent}/Images/{*filename}",
            new { subagent = "aq", controller = "Image", action = "Show", id = UrlParameter.Optional, culture = "en"},  new[] { "aq3.Controllers" }
        );

        /* localized routes follow */
        routes.MapRoute(
            "Localized Images",
            "Images/{*filename}",
            new { subagent = "aq", controller = "Image", action = "Show", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        routes.MapRoute(
            "Blog Posts",
            "Blog/{*postname}",
            new { subagent = "aq", controller = "Blog", action = "Index", id = UrlParameter.Optional},  new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        routes.MapRoute(
            "Office Posts",
            "Office/{*address}",
            new { subagent = "aq", controller = "Offices", action = "Address", id = UrlParameter.Optional }, new[] { "aq3.Controllers" }
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        routes.MapRoute(
             "Default", // Route name
             "{controller}/{action}/{id}", // URL with parameters
             new { subagent = "aq", controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "aq3.Controllers" } // Parameter defaults
        ).RouteHandler = new MultiCultureMvcRouteHandler();

        foreach (System.Web.Routing.Route r in routes)
        {
            if (r.RouteHandler is MultiCultureMvcRouteHandler)
            {
                r.Url = "{subagent}/{culture}/" + r.Url;
                //Adding default culture 
                if (r.Defaults == null)
                {
                    r.Defaults = new RouteValueDictionary();
                }
                r.Defaults.Add("culture", Culture.en.ToString());

                //Adding constraint for culture param
                if (r.Constraints == null)
                {
                    r.Constraints = new RouteValueDictionary();
                }
                r.Constraints.Add("culture", new CultureConstraint(Culture.en.ToString(), Culture.es.ToString()));
            }
        }

    }

Hai ragione a suggerire la pratica POST REDIRECT, infatti lo faccio per quasi tutte le azioni post. Tuttavia avevo un'esigenza molto particolare: ho un modulo di filtro in cima alla pagina, inizialmente inviato con get. Ma ho riscontrato un problema con un campo data non associato e poi ho scoperto che le richieste GET non portano la cultura in giro (io uso il francese per la mia app), quindi ho dovuto cambiare la richiesta su POST per legare correttamente la mia data. Poi è arrivato questo problema, sono un po 'bloccato con lei ..
Souhaieb Besbes

@SouhaiebBesbes Guarda i miei aggiornamenti che mostrano come gestisco la cultura.
B2K

@SouhaiebBesbes forse un po 'più semplice sarebbe memorizzare la tua cultura in TempData. Vedi stackoverflow.com/questions/12422930/…
B2K

0

Bene, questo sembrava funzionare sulla mia pagina Razor e non ha mai nemmeno fatto un viaggio di andata e ritorno al file .cs. Questo è il vecchio modo HTML. Potrebbe essere utile.

<input type="reset" value="Reset">
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.