Asp.net MVC ModelState.Clear

116

Czy ktoś może mi podać zwięzłą definicję roli ModelState w Asp.net MVC (lub link do niej). W szczególności muszę wiedzieć, w jakich sytuacjach dzwonienie jest konieczne lub pożądane ModelState.Clear().

Trochę otwarte, huh ... przepraszam, myślę, że może pomóc, jeśli powiem ci, co naprawdę robię:

Mam akcję edycji na kontrolerze o nazwie „Strona”. Kiedy po raz pierwszy widzę formularz do zmiany szczegółów strony, wszystko ładuje się poprawnie (wiązanie z obiektem „MyCmsPage”). Następnie klikam przycisk, który generuje wartość dla jednego z pól obiektu MyCmsPage ( MyCmsPage.SeoTitle). Generuje dobrze i aktualizuje obiekt, a następnie zwracam wynik akcji z nowo zmodyfikowanym obiektem strony i oczekuję, że odpowiednie pole tekstowe (renderowane przy użyciu <%= Html.TextBox("seoTitle", page.SeoTitle)%>) zostanie zaktualizowane ... ale niestety wyświetla wartość ze starego modelu, który został załadowany.

Pracowałem nad tym, używając, ModelState.Clear()ale muszę wiedzieć, dlaczego / jak to działa, więc nie robię tego na ślepo.

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>
Panie Grok
źródło
Noob AspMVC, jeśli chce buforować stare dane, to po co znowu dawać model użytkownikowi: @ Miałem ten sam problem, wielkie dzięki brachu
deadManN

Odpowiedzi:

135

Myślę, że to błąd w MVC. Walczyłem z tym problemem godzinami dzisiaj.

Biorąc to pod uwagę:

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

Widok jest renderowany z oryginalnym modelem, ignorując zmiany. Pomyślałem więc, że może nie podoba mi się używanie tego samego modelu, więc spróbowałem tak:

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

I nadal widok renderuje się z oryginalnym modelem. Dziwne jest to, że kiedy umieszczam punkt przerwania w widoku i badam model, ma on zmienioną wartość. Ale strumień odpowiedzi ma stare wartości.

W końcu odkryłem tę samą pracę, którą zrobiłeś:

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

Działa zgodnie z oczekiwaniami.

Nie sądzę, żeby to była „funkcja”, prawda?

Tim Scott
źródło
33
Zrobiłem prawie dokładnie to samo co ty. Okazało się, że to nie jest błąd. To jest zgodne z projektem: błąd? EditorFor i DisplayFor nie wyświetlają tej samej wartości, a pomocnicy HTML ASP.NET MVC
Metro Smurf
8
Człowieku, walczyłem z nim już 2 godziny. Dzięki za opublikowanie tej odpowiedzi!
Andrey Agibalov
37
to nadal prawda i wiele osób, w tym ja, traci z tego powodu dużo czasu. błąd lub projekt, nie obchodzi mnie to, to „nieoczekiwane”.
Proviste
7
Zgadzam się z @Proviste, mam nadzieję, że ta „funkcja” zostanie w przyszłości usunięta
Ben
8
Właśnie spędziłem nad tym cztery godziny. Brzydki.
Brian MacKay,
46

Aktualizacja:

  • To nie jest błąd.
  • Przestań wracać View()z akcji POST. Zamiast tego użyj PRG i przekieruj do GET, jeśli akcja zakończy się sukcesem.
  • Jeśli Zwracanie View()z działania POST, zrób to dla walidacji formularza i zrobić to tak MVC został zaprojektowany przy użyciu wbudowanego w pomocników. Jeśli zrobisz to w ten sposób, nie powinieneś używać.Clear()
  • Jeśli używasz tej akcji, aby zwrócić ajax dla SPA , użyj kontrolera interfejsu API sieci Web i zapomnij o tym, ModelStateponieważ i tak nie powinieneś go używać.

Stara odpowiedź:

ModelState w MVC służy przede wszystkim do opisywania stanu obiektu modelu w dużej mierze w odniesieniu do tego, czy ten obiekt jest prawidłowy, czy nie. Ten samouczek powinien wiele wyjaśnić.

Zasadniczo nie ma potrzeby czyszczenia ModelState, ponieważ jest on obsługiwany przez silnik MVC. Wyczyszczenie go ręcznie może spowodować niepożądane rezultaty podczas próby przestrzegania najlepszych praktyk walidacji MVC.

Wygląda na to, że próbujesz ustawić domyślną wartość tytułu. Należy to zrobić, gdy instancja obiektu modelu jest tworzona (warstwa domeny gdzieś lub w samym obiekcie - bez parametrów), przy akcji get tak, że przechodzi do strony za pierwszym razem lub całkowicie na kliencie (przez ajax lub coś w tym rodzaju) aby wyglądało to tak, jakby użytkownik je wprowadził i wraca z kolekcją opublikowanych formularzy. W pewnym sensie Twoje podejście do dodawania tej wartości podczas odbierania kolekcji formularzy (w akcji POST // Edycja) powoduje to dziwne zachowanie, które może sprawić, że .Clear() pozornie zadziała. Zaufaj mi - nie chcesz używać przezroczystego. Wypróbuj inny pomysł.

Matt Kocaj
źródło
1
Pomaga mi trochę przemyśleć moją warstwę usług (jęk, ale thx), ale podobnie jak w przypadku wielu rzeczy w sieci, pochyla się mocno w kierunku używania ModelState do walidacji.
Pan Grok
Dodano więcej informacji do pytania, aby pokazać, dlaczego szczególnie interesuję się ModelState.Clear () i powód mojego zapytania
Mr Grok
5
Naprawdę nie kupuję tego argumentu, aby przestać zwracać View (...) z funkcji [HttpPost]. Jeśli publikujesz zawartość za pośrednictwem ajax, a następnie aktualizujesz dokument za pomocą wynikowego PartialView, MVC ModelState został pokazany jako niepoprawny. Jedynym rozwiązaniem, jakie znalazłem, jest wyczyszczenie go w metodzie kontrolera.
Aaron Hudon
@AaronHudon PRG jest dość dobrze ugruntowany.
Matt Kocaj,
Jeśli POST z wywołaniem AJAX mogę przekierować do akcji GET i zwrócić widok wypełniony modelem, jak chce OP, wszystko asynchronicznie?
MyiEye,
17

Jeśli chcesz wyczyścić wartość dla pojedynczego pola, uważam, że przydatna jest następująca technika.

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

Uwaga: Zmień „Klucz” na nazwę pola, które chcesz zresetować.

Carl Saunders
źródło
Nie wiem, dlaczego to zadziałało u mnie inaczej (może MVC4)? Ale później musiałem też zrobić model.Key = "". Wymagane są obie linie.
TTT,
Chciałbym pogratulować usunięcia komentarza @PeterGluck. Jest to lepsze niż wyczyszczenie całego stanu modelu (ponieważ mam błędy w niektórych polach, które chciałbym zachować).
Tjab
6

Cóż, ModelState w zasadzie przechowuje bieżący stan modelu pod względem walidacji

ModelErrorCollection: Reprezentuj błędy, gdy model próbuje powiązać wartości. dawny.

TryUpdateModel();
UpdateModel();

lub jak parametr w ActionResult

public ActionResult Create(Person person)

ValueProviderResult : Przechowuj szczegóły dotyczące próby powiązania z modelem. dawny. AttemptedValue, Culture, RawValue .

Należy zachować ostrożność podczas używania metody Clear (), ponieważ może ona prowadzić do niespodziewanych wyników. I stracisz kilka fajnych właściwości ModelState, takich jak AttemptedValue, jest to używane przez MVC w tle do ponownego wypełnienia wartości formularza w przypadku błędu.

ModelState["a"].Value.AttemptedValue
JOBG
źródło
1
hmmm ... To może być miejsce, w którym dostaję problem, patrząc na to. Sprawdziłem wartość właściwości Model.SeoTitle i zmieniła się, ale próbowana wartość nie. Wygląda na to, że zapisuje wartość tak, jakby na stronie wystąpił błąd, mimo że go nie ma (sprawdzono słownik ModelState i nie ma żadnych błędów).
Pan Grok
6

Miałem przypadek, w którym chciałem zaktualizować model sumowanego formularza i nie chciałem przekierowywać do działania ze względu na wydajność. Poprzednie wartości ukrytych pól były zachowywane w moim zaktualizowanym modelu - powodując różnego rodzaju problemy !.

Kilka wierszy kodu szybko zidentyfikowało elementy w ModelState, które chciałem usunąć (po walidacji), więc nowe wartości zostały użyte w postaci: -

while (ModelState.FirstOrDefault(ms => ms.Key.ToString().StartsWith("SearchResult")).Value != null)
{
    ModelState.Remove(ModelState.FirstOrDefault(ms => ms.Key.ToString().StartsWith("SearchResult")));
}
stevieg
źródło
5

Cóż, wydaje się, że wielu z nas zostało przez to ugryzionych i chociaż powód, dla którego tak się dzieje, ma sens, potrzebowałem sposobu, aby upewnić się, że wartość mojego Modelu została pokazana, a nie ModelState.

Niektórzy sugerowali ModelState.Remove(string key), ale nie jest oczywiste, co keypowinno być, szczególnie w przypadku modeli zagnieżdżonych. Oto kilka metod, które wymyśliłem, aby w tym pomóc.

RemoveStateForMetoda zajmie ModelStateDictionary, model i wyrażenie dla żądanej własności i usuń go. HiddenForModelmożna użyć w widoku, aby utworzyć ukryte pole wejściowe przy użyciu tylko wartości z modelu, najpierw usuwając jego wpis ModelState. (Można to łatwo rozszerzyć dla innych metod rozszerzenia pomocnika).

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

Zadzwoń z kontrolera takiego jak ten:

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

lub z takiego widoku:

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

Używa System.Web.Mvc.ExpressionHelperdo uzyskania nazwy właściwości ModelState.

Tobias J.
źródło
1
Bardzo dobrze! Utrzymanie zakładki dla funkcji ExpressionHelper.
Gerard ONeill
4

Chciałem zaktualizować lub zresetować wartość, jeśli nie została ona do końca zweryfikowana, i napotkałem ten problem.

Prosta odpowiedź, ModelState.Remove, jest ... problematyczna ... ponieważ jeśli używasz pomocników, tak naprawdę nie znasz nazwy (chyba że trzymasz się konwencji nazewnictwa). Chyba że utworzysz funkcję, której zarówno niestandardowy pomocnik, jak i kontroler będą mogły użyć do uzyskania nazwy.

Ta funkcja powinna zostać zaimplementowana jako opcja w pomocniku, gdzie domyślnie nie jest to możliwe , ale jeśli chcesz, aby nieakceptowane dane wejściowe zostały ponownie wyświetlone, możesz to po prostu powiedzieć.

Ale przynajmniej teraz rozumiem problem;).

Gerard ONeill
źródło
Musiałem to zrobić dokładnie; zobacz moje metody, które zamieściłem poniżej, które pomogły mi Remove()we właściwym kluczu.
Tobias J
0

W końcu to rozumiem. Mój niestandardowy modelBinder, który nie był rejestrowany i robi to:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

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

Więc coś, co robiło domyślne powiązanie modelu, musiało być przyczyną problemu. Nie wiem co, ale mój problem został przynajmniej rozwiązany teraz, gdy mój segregator modelu niestandardowego jest rejestrowany.

Panie Grok
źródło
Cóż, nie mam doświadczenia z niestandardowym ModelBinder, domyślny jak dotąd pasuje do moich potrzeb =).
JOBG
0

Generalnie, gdy walczysz ze standardowymi praktykami ram, nadszedł czas, aby ponownie rozważyć swoje podejście. W takim przypadku zachowanie ModelState. Na przykład, jeśli nie chcesz, aby stan modelu po POST, rozważ przekierowanie do 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);
}

EDYTOWANO, aby odpowiedzieć na komentarz dotyczący kultury

Oto, czego używam do obsługi wielokulturowej aplikacji MVC. Najpierw podklasy obsługi trasy:

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
}

A oto jak łączę trasy. Po utworzeniu tras dodaję mojego subagenta (example.com/subagent1, example.com/subagent2 itd.), A następnie kod kultury. Jeśli potrzebujesz tylko kultury, po prostu usuń subagenta z programów obsługi tras i tras.

    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
źródło
Masz rację sugerując praktykę POST REDIRECT, w rzeczywistości robię to dla prawie każdej akcji postu. Jednak miałem bardzo szczególną potrzebę: mam formularz filtru na górze strony, początkowo przesłany za pomocą get. Ale napotkałem problem z polem daty, które nie jest związane, a następnie odkryłem, że żądania GET nie zawierają kultury (używam języka francuskiego w mojej aplikacji), więc musiałem zmienić żądanie na POST, aby pomyślnie powiązać moją datę. Potem pojawił się ten problem, trochę ją utknąłem ...
Souhaieb Besbes
@SouhaiebBesbes Zobacz moje aktualizacje pokazujące, jak radzę sobie z kulturą.
B2K
@SouhaiebBesbes być może nieco prostsze byłoby przechowywanie kultury w TempData. Zobacz stackoverflow.com/questions/12422930/…
B2K
0

Cóż, wydawało się, że to działa na mojej stronie Razor i nigdy nawet nie przeszedłem w obie strony do pliku .cs. To jest stary sposób HTML. Może się przydać.

<input type="reset" value="Reset">
JustJohn
źródło