Asp.net MVC ModelState.Clear

116

Alguém pode me dar uma definição sucinta da função de ModelState no Asp.net MVC (ou um link para um). Em particular, preciso saber em que situações é necessário ou desejável ligar ModelState.Clear().

Um pouco aberto acabou hein ... desculpe, eu acho que pode ajudar se te contar o que estou fazendo de forma aguda:

Eu tenho uma ação de edição em um controlador chamado "Página". Quando vejo o formulário para alterar os detalhes da página, tudo carrega bem (vinculado a um objeto "MyCmsPage"). Em seguida, clico em um botão que gera um valor para um dos campos do objeto MyCmsPage ( MyCmsPage.SeoTitle). Ele gera e atualiza o objeto e eu, então, retorno o resultado da ação com o objeto da página recém-modificado e espero que a caixa de texto relevante (renderizada usando <%= Html.TextBox("seoTitle", page.SeoTitle)%>) seja atualizada ... mas, infelizmente, exibe o valor do modelo antigo que foi carregado.

Eu trabalhei em torno disso usando, ModelState.Clear()mas preciso saber por que / como funcionou, então não estou fazendo isso às cegas.

Controlador de página:

[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>
Sr. Grok
fonte
Noob AspMVC, se ele quer armazenar dados antigos, então qual é o ponto em dar o modelo para o usuário novamente: @ eu tive o mesmo problema, muito obrigado mano
deadManN 01 de

Respostas:

135

Acho que é um bug no MVC. Lutei com esse problema por horas hoje.

Dado isso:

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

A visualização é renderizada com o modelo original, ignorando as alterações. Então pensei, talvez ele não goste de eu usar o mesmo modelo, então tentei assim:

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

E ainda assim a visualização renderiza com o modelo original. O que é estranho é que, quando coloco um ponto de interrupção na visualização e examino o modelo, ele tem o valor alterado. Mas o fluxo de resposta tem os valores antigos.

Eventualmente, descobri o mesmo trabalho em torno que você fez:

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

Funciona conforme o esperado.

Não acho que isso seja um "recurso", não é?

Tim Scott
fonte
33
Apenas fez quase exatamente a mesma coisa que você. No entanto, descobri que isso não é um bug. É por design: um bug? EditorFor e DisplayFor não exibem o mesmo valor e os auxiliares de HTML da ASP.NET MVC renderizam o valor errado
Metro Smurf
8
Cara, já passei 2 horas lutando com isso. Obrigado por postar esta resposta!
Andrey Agibalov
37
isso ainda é verdade e muitas pessoas, inclusive eu, estão perdendo muito tempo por causa disso. bug ou por design, eu não me importo, é "inesperado".
Proviste
7
Eu concordo com @Proviste, espero que esse "recurso" seja removido no futuro
Ben
8
Acabei de passar quatro horas nisso. Feio.
Brian MacKay de
46

Atualizar:

  • Este não é um bug.
  • Pare de retornar View()de uma ação POST. Em vez disso, use PRG e redirecione para GET se a ação for um sucesso.
  • Se você estiver retornando um View()de uma ação POST, faça-o para validação do formulário e faça-o da maneira que o MVC foi projetado usando os auxiliares integrados. Se você fizer isso dessa forma, não precisará usar.Clear()
  • Se você estiver usando essa ação para retornar ajax para um SPA , use um controlador de API da web e esqueça, ModelStatepois você não deveria usá-lo de qualquer maneira.

Resposta antiga:

ModelState em MVC é usado principalmente para descrever o estado de um objeto de modelo em grande parte com relação a se esse objeto é válido ou não. Este tutorial deve explicar muito.

Geralmente, você não precisa limpar o ModelState, pois ele é mantido pelo mecanismo MVC para você. Limpá-lo manualmente pode causar resultados indesejados ao tentar aderir às melhores práticas de validação MVC.

Parece que você está tentando definir um valor padrão para o título. Isso deve ser feito quando o objeto do modelo é instanciado (camada de domínio em algum lugar ou no próprio objeto - ctor sem parâmetros), na ação get de forma que desça para a página pela primeira vez ou completamente no cliente (via ajax ou algo assim) de modo que parece que o usuário o inseriu e ele volta com a coleção de formulários postados. De alguma forma, a sua abordagem de adicionar este valor ao receber uma coleção de formulários (na ação POST // Editar) está causando esse comportamento bizarro que pode .Clear() parecer funcionar para você. Confie em mim - você não quer usar o claro. Experimente uma das outras ideias.

Matt Kocaj
fonte
1
Isso me ajuda a repensar minha camada de serviços um pouco (resmungo, mas obrigado), mas como com um monte de coisas na rede, ele se inclina fortemente para o ponto de vista de usar ModelState para validação.
Sr. Grok
Adicionadas mais informações à pergunta para mostrar por que estou particularmente interessado em ModelState.Clear () e o motivo da minha consulta
Sr. Grok
5
Eu realmente não aceito esse argumento para parar de retornar View (...) de uma função [HttpPost]. Se você estiver fazendo um POST de conteúdo via ajax e, em seguida, atualizando o documento com o PartialView resultante, o MVC ModelState mostrou estar incorreto. A única solução alternativa que encontrei é limpá-la no método do controlador.
Aaron Hudon
@AaronHudon PRG está muito bem estabelecido.
Matt Kocaj
Se eu POSTAR com uma chamada AJAX, posso redirecionar para uma ação GET e retornar uma visualização preenchida pelo modelo como o OP deseja, tudo de forma assíncrona?
MyiEye
17

Se você deseja limpar um valor para um campo individual, achei a seguinte técnica útil.

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

Observação: altere "Chave" para o nome do campo que você deseja redefinir.

Carl Saunders
fonte
Eu não sei por que isso funcionou de forma diferente para mim (MVC4 talvez)? Mas eu também tive que fazer model.Key = "" depois. Ambas as linhas são obrigatórias.
TTT de
Gostaria de cumprimentá-lo pelo comentário de remoção @PeterGluck. É melhor do que limpar o modelstate completo (já que tenho erros em alguns campos que gostaria de manter).
Tjab de
6

Bem, o ModelState basicamente mantém o estado atual do modelo em termos de validação, ele mantém

ModelErrorCollection: Representa os erros quando o modelo tenta vincular os valores. ex.

TryUpdateModel();
UpdateModel();

ou como um parâmetro no ActionResult

public ActionResult Create(Person person)

ValueProviderResult : contém os detalhes sobre a tentativa de vinculação ao modelo. ex. AttemptedValue, Culture, RawValue .

O método Clear () deve ser usado com cuidado, pois pode levar a resultados inesperados. E você perderá algumas propriedades interessantes do ModelState como AttemptedValue, isso é usado pelo MVC em segundo plano para preencher novamente os valores do formulário em caso de erro.

ModelState["a"].Value.AttemptedValue
JOBG
fonte
1
Hmmm ... Isso pode ser onde estou obtendo o problema pelo que parece. Eu inspecionei o valor da propriedade Model.SeoTitle e ele mudou, mas o valor tentado não. Parece que está inserindo o valor como se houvesse um erro na página, embora não haja nenhum (verifique o dicionário ModelState e não há erros).
Sr. Grok
6

Tive uma instância em que queria atualizar o modelo de um formulário submetido e não queria 'Redirecionar para a ação' por motivos de desempenho. Os valores anteriores de campos ocultos estavam sendo mantidos em meu modelo atualizado - causando todos os tipos de problemas!

Algumas linhas de código logo identificaram os elementos dentro de ModelState que eu queria remover (após a validação), então os novos valores foram usados ​​na forma: -

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

Bem, muitos de nós parecem ter sido mordidos por isso, e embora o motivo pelo qual isso acontece faça sentido, eu precisava de uma maneira de garantir que o valor em meu Model fosse mostrado, e não ModelState.

Alguns sugeriram ModelState.Remove(string key), mas não é óbvio o que keydeveria ser, especialmente para modelos aninhados. Aqui estão alguns métodos que criei para ajudar nisso.

O RemoveStateFormétodo pegará um ModelStateDictionary, um Modelo e uma expressão para a propriedade desejada e os removerá. HiddenForModelpode ser usado em sua Visualização para criar um campo de entrada oculto usando apenas o valor do Modelo, removendo primeiro sua entrada ModelState. (Isso pode ser facilmente expandido para os outros métodos de extensão auxiliares).

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

Chame de um controlador como este:

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

ou de uma visão como esta:

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

Ele usa System.Web.Mvc.ExpressionHelperpara obter o nome da propriedade ModelState.

Tobias J
fonte
1
Muito agradável! Manter um guia sobre isso para a funcionalidade ExpressionHelper.
Gerard ONeill
4

Eu queria atualizar ou redefinir um valor se ele não fosse validado e encontrei este problema.

A resposta fácil, ModelState.Remove, é ... problemática ... porque se você estiver usando ajudantes, você não sabe realmente o nome (a menos que siga a convenção de nomenclatura). A menos que você crie uma função que tanto seu auxiliar personalizado quanto seu controlador possam usar para obter um nome.

Este recurso deveria ter sido implementado como uma opção no helper, onde por padrão não faz isso, mas se você quiser que a entrada não aceita seja exibida novamente, você pode apenas dizer isso.

Mas pelo menos eu entendo o problema agora;).

Gerard ONeill
fonte
Eu precisava fazer exatamente isso; veja meus métodos que postei abaixo que me ajudaram Remove()na chave correta.
Tobias J
0

Entendi no final. Meu ModelBinder personalizado que não estava sendo registrado e faz o seguinte:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

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

Portanto, algo que a vinculação do modelo padrão estava fazendo deve ter causado o problema. Não tenho certeza do que, mas pelo menos meu problema foi corrigido agora que meu fichário de modelo personalizado está sendo registrado.

Sr. Grok
fonte
Bem, eu não tenho experiência com um ModelBinder personalizado, o padrão atende às minhas necessidades até agora =).
JOBG
0

Geralmente, quando você está lutando contra as práticas padrão de uma estrutura, é hora de reconsiderar sua abordagem. Nesse caso, o comportamento de ModelState. Por exemplo, quando você não quer o estado do modelo após um POST, considere um redirecionamento para o 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);
}

EDITADO para responder a comentários culturais:

Aqui está o que eu uso para lidar com um aplicativo MVC multicultural. Primeiro, as subclasses do gerenciador de rota:

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
}

E é assim que faço a conexão das rotas. Depois de criar as rotas, acrescento meu subagente (example.com/subagent1, example.com/subagent2, etc) e o código de cultura. Se tudo que você precisa é a cultura, simplesmente remova o subagente dos manipuladores de rota e rotas.

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

    }
B2K
fonte
Você está muito certo em sugerir a prática PÓS REDIRECIONAR, na verdade eu faço isso para quase todas as ações pós. No entanto, eu tinha uma necessidade muito particular: tenho um formulário de filtro no topo da página, inicialmente enviado com get. Mas eu tive um problema com um campo de data não sendo vinculado e então descobri que as solicitações GET não carregam a cultura por aí (eu uso francês para meu aplicativo), então eu tive que mudar a solicitação para POST para vincular minha data com sucesso. Aí veio esse problema, estou um pouco preso a ela ..
Souhaieb Besbes
@SouhaiebBesbes Veja minhas atualizações mostrando como eu lido com a cultura.
B2K
@SouhaiebBesbes talvez um pouco mais simples seja armazenar sua cultura em TempData. Consulte stackoverflow.com/questions/12422930/…
B2K
0

Bem, isso pareceu funcionar na minha página do Razor e nunca fiz uma viagem de ida e volta para o arquivo .cs. Esta é a velha maneira de html. Pode ser útil.

<input type="reset" value="Reset">
JustJohn
fonte