Asp.net MVC ModelState.Clear

116

¿Alguien puede darme una definición sucinta del papel de ModelState en Asp.net MVC (o un enlace a uno)? En particular, necesito saber en qué situaciones es necesario o deseable llamar ModelState.Clear().

Un poco abierto eh ... lo siento, creo que podría ayudar si les digo lo que estoy haciendo en realidad:

Tengo una acción de edición en un controlador llamado "Página". Cuando veo por primera vez el formulario para cambiar los detalles de la página, todo se carga bien (enlazando a un objeto "MyCmsPage"). Luego hago clic en un botón que genera un valor para uno de los campos del objeto MyCmsPage ( MyCmsPage.SeoTitle). Genera bien y actualiza el objeto y luego devuelvo el resultado de la acción con el objeto de página recién modificado y espero <%= Html.TextBox("seoTitle", page.SeoTitle)%>que se actualice el cuadro de texto relevante (renderizado usando ) ... pero lamentablemente muestra el valor del modelo anterior que se cargó.

Lo he solucionado usando, ModelState.Clear()pero necesito saber por qué / cómo ha funcionado, así que no lo estoy haciendo a ciegas.

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>
Sr. Grok
fuente
Noob AspMVC, si quiere almacenar en caché datos antiguos, ¿cuál es el punto de volver a dar el modelo al usuario ?: @ Tuve el mismo problema, muchas gracias hermano
deadManN

Respuestas:

135

Creo que es un error en MVC. Hoy luché con este problema durante horas.

Dado este:

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

La vista se renderiza con el modelo original, ignorando los cambios. Entonces pensé, tal vez no me gusta que use el mismo modelo, así que intenté así:

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

Y aún así, la vista se reproduce con el modelo original. Lo extraño es que cuando pongo un punto de interrupción en la vista y examino el modelo, tiene el valor cambiado. Pero el flujo de respuesta tiene los valores antiguos.

Finalmente descubrí el mismo trabajo que tú hiciste:

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

Funciona como se esperaba.

No creo que esto sea una "característica", ¿verdad?

Tim Scott
fuente
33
Hice casi exactamente lo mismo que tú. Sin embargo, descubrí que esto no es un error. Es por diseño: ¿ Un error? EditorFor y DisplayFor no muestran el mismo valor y los ayudantes HTML de ASP.NET MVC representan el valor incorrecto
Metro Smurf
8
Hombre, ya he pasado 2 horas peleando con él. ¡Gracias por publicar esta respuesta!
Andrey Agibalov
37
esto sigue siendo cierto y mucha gente, incluyéndome a mí, está perdiendo mucho tiempo debido a esto. error o por diseño, no me importa, es "inesperado".
Proviste
7
Estoy de acuerdo con @Proviste, espero que esta "característica" se elimine en el futuro
Ben
8
Solo pasé cuatro horas en esto. Feo.
Brian MacKay
46

Actualizar:

  • Esto no es un error.
  • Deje de regresar View()de una acción POST. Utilice PRG en su lugar y redirija a un GET si la acción es un éxito.
  • Si está devolviendo una View()acción de POST, hágalo para la validación del formulario y hágalo de la forma en que MVC está diseñado utilizando los ayudantes integrados. Si lo hace de esta manera, no debería necesitar usar.Clear()
  • Si está usando esta acción para devolver ajax para un SPA , use un controlador web api y olvídese, ModelStateya que no debería usarlo de todos modos.

Respuesta anterior:

ModelState en MVC se usa principalmente para describir el estado de un objeto modelo en gran parte con relación a si ese objeto es válido o no. Este tutorial debería explicar mucho.

En general, no debería necesitar borrar ModelState, ya que el motor MVC lo mantiene. Borrarlo manualmente puede causar resultados no deseados al intentar cumplir con las mejores prácticas de validación de MVC.

Parece que está intentando establecer un valor predeterminado para el título. Esto debe hacerse cuando se crea una instancia del objeto modelo (capa de dominio en algún lugar o en el objeto mismo - ctor sin parámetros), en la acción de obtención de modo que baje a la página la primera vez o completamente en el cliente (a través de ajax o algo así) para que parezca que el usuario lo ingresó y regresa con la colección de formularios publicados. De alguna manera, su enfoque de agregar este valor al recibir una colección de formularios (en la acción POST // Editar) está causando este comportamiento extraño que podría resultar en que .Clear() parezca que funciona para usted. Créame, no querrá usar el claro. Pruebe una de las otras ideas.

Matt Kocaj
fuente
1
Me ayuda a repensar un poco mi capa de servicios (gemir pero gracias) pero, como ocurre con muchas cosas en la red, se inclina mucho hacia el punto de vista de usar ModelState para la validación.
Mr Grok
Se agregó más información a la pregunta para mostrar por qué estoy particularmente interesado en ModelState.Clear () y el motivo de mi consulta
Sr. Grok
5
Realmente no compro este argumento para dejar de devolver View (...) desde una función [HttpPost]. Si publica contenido a través de ajax y luego actualiza el documento con el PartialView resultante, se ha demostrado que MVC ModelState es incorrecto. La única solución que he encontrado es borrarla en el método del controlador.
Aaron Hudon
@AaronHudon PRG está bastante bien establecido.
Matt Kocaj
Si publico con una llamada AJAX, ¿puedo redirigir a una acción GET y devolver una vista llena de modelo como el OP quiere, todo de forma asincrónica?
MyiEye
17

Si desea borrar un valor para un campo individual, la siguiente técnica me resultó útil.

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

Nota: Cambie "Clave" por el nombre del campo que desea restablecer.

Carl Saunders
fuente
No sé por qué esto funcionó de manera diferente para mí (quizás MVC4). Pero también tuve que hacer model.Key = "" después. Se requieren ambas líneas.
TTT
Me gustaría felicitarlo por el comentario eliminado @PeterGluck. Es mejor que borrar el estado del modelo completo (ya que tengo errores en algunos campos que me gustaría conservar).
Tjab
6

Bueno, el ModelState básicamente mantiene el estado actual del modelo en términos de validación, mantiene

ModelErrorCollection: representa los errores cuando el modelo intenta vincular los valores. ex.

TryUpdateModel();
UpdateModel();

o como un parámetro en ActionResult

public ActionResult Create(Person person)

ValueProviderResult : contiene los detalles sobre el intento de enlace con el modelo. ex. AttemptedValue, Culture, RawValue .

El método Clear () debe usarse con precaución porque puede dar lugar a resultados inesperados. Y perderá algunas propiedades agradables del ModelState como AttemptedValue, esto es utilizado por MVC en segundo plano para volver a llenar los valores del formulario en caso de error.

ModelState["a"].Value.AttemptedValue
JOBG
fuente
1
hmmm ... Eso podría ser donde estoy obteniendo el problema por lo que parece. Inspeccioné el valor de la propiedad Model.SeoTitle y ha cambiado, pero el valor intentado no. Parece que está pegando el valor como si hubiera un error en la página aunque no lo haya (revisé el Diccionario ModelState y no hay errores).
Mr Grok
6

Tuve una instancia en la que quería actualizar el modelo de un formulario enviado y no quería 'Redirigir a la acción' por motivos de rendimiento. Los valores anteriores de los campos ocultos se conservaban en mi modelo actualizado, ¡lo que causaba todo tipo de problemas!

Algunas líneas de código pronto identificaron los elementos dentro de ModelState que quería eliminar (después de la validación), por lo que los nuevos valores se usaron en el formulario: -

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

Bueno, parece que muchos de nosotros hemos sido mordidos por esto, y aunque la razón por la que esto sucede tiene sentido, necesitaba una forma de asegurarme de que se mostrara el valor en mi modelo, y no en ModelState.

Algunos han sugerido ModelState.Remove(string key), pero no es obvio qué keydebería ser, especialmente para los modelos anidados. Aquí hay un par de métodos que se me ocurrieron para ayudar con esto.

El RemoveStateFormétodo tomará un ModelStateDictionary, un modelo y una expresión para la propiedad deseada y la eliminará. HiddenForModelse puede usar en su Vista para crear un campo de entrada oculto usando solo el valor del Modelo, eliminando primero su entrada ModelState. (Esto podría expandirse fácilmente para los otros métodos de extensión de ayuda).

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

Llame desde un controlador como este:

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

o desde una vista como esta:

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

Se usa System.Web.Mvc.ExpressionHelperpara obtener el nombre de la propiedad ModelState.

Tobias J
fuente
1
¡Muy agradable! Mantener una pestaña en esto para la funcionalidad ExpressionHelper.
Gerard ONeill
4

Quería actualizar o restablecer un valor si no se validaba del todo y encontré este problema.

La respuesta fácil, ModelState.Remove, es ... problemática ... porque si estás usando ayudantes realmente no conoces el nombre (a menos que sigas la convención de nomenclatura). A menos que tal vez cree una función que tanto su asistente personalizado como su controlador puedan usar para obtener un nombre.

Esta función debería haberse implementado como una opción en el asistente, donde de forma predeterminada no hace esto, pero si desea que se vuelva a mostrar la entrada no aceptada, simplemente puede decirlo.

Pero al menos ahora entiendo el problema;).

Gerard ONeill
fuente
Necesitaba hacer exactamente esto; vea mis métodos que publiqué a continuación, que me ayudaron a encontrar Remove()la clave correcta.
Tobias J
0

Lo tengo al final. Mi ModelBinder personalizado que no se estaba registrando y hace esto:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

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

Entonces, algo que estaba haciendo el enlace del modelo predeterminado debe haber causado el problema. No estoy seguro de qué, pero mi problema al menos está solucionado ahora que se está registrando mi carpeta de modelos personalizados.

Sr. Grok
fuente
Bueno, no tengo experiencia con un ModelBinder personalizado, el predeterminado se adapta a mis necesidades hasta ahora =).
JOBG
0

Generalmente, cuando se encuentra luchando contra un marco de prácticas estándar, es hora de reconsiderar su enfoque. En este caso, el comportamiento de ModelState. Por ejemplo, cuando no desee el estado del modelo después de una POST, considere una redirección al archivo 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 al comentario cultural:

Esto es lo que uso para manejar una aplicación MVC multicultural. Primero, las subclases del controlador de ruta:

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
}

Y así es como conecto las rutas. Después de crear las rutas, antepongo mi subagente (example.com/subagent1, example.com/subagent2, etc.) y luego el código de cultura. Si todo lo que necesita es la cultura, simplemente elimine el subagente de los controladores de ruta y las rutas.

    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
fuente
Tienes razón al sugerir la práctica POST REDIRECT, de hecho, hago esto para casi todas las acciones posteriores. Sin embargo, tenía una necesidad muy particular: tengo un formulario de filtro en la parte superior de la página, inicialmente se envió con get. Pero encontré un problema con un campo de fecha que no estaba vinculado y luego descubrí que las solicitudes GET no llevan la cultura (uso el francés para mi aplicación), así que tuve que cambiar la solicitud a POST para vincular con éxito mi fecha. Luego vino este problema, estoy un poco atrapado con ella ..
Souhaieb Besbes
@SouhaiebBesbes Vea mis actualizaciones que muestran cómo manejo la cultura.
B2K
@SouhaiebBesbes quizás un poco más simple sería almacenar su cultura en TempData. Ver stackoverflow.com/questions/12422930/…
B2K
0

Bueno, esto pareció funcionar en mi página de Razor y ni siquiera hizo un viaje de ida y vuelta al archivo .cs. Esta es la vieja forma html. Puede ser útil.

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