Asp.net MVC ModelState.Clear


116

Asp.net MVCでのModelStateの役割の簡潔な定義(またはリンクへのリンク)を誰かに教えてもらえますか?特に、どのような状況で電話をする必要があるか、または望ましいかを知る必要がありますModelState.Clear()

ビットオープンエンドハァ ...申し訳ありませんが、私が実際に何をしているのかを教えてくれると助かると思います:

「ページ」というコントローラーで編集アクションがあります。最初にページの詳細を変更するフォームを表示すると、すべてが正常にロードされます(「MyCmsPage」オブジェクトにバインド)。次に、MyCmsPageオブジェクトのフィールドのいずれかの値を生成するボタンをクリックします(MyCmsPage.SeoTitle)。それはうまく生成し、オブジェクトを更新し、新しく変更されたページオブジェクトでアクション結果を返し、関連するテキストボックス(を使用してレンダリングされた<%= Html.TextBox("seoTitle", page.SeoTitle)%>)が更新されることを期待します...しかし、ロードされた古いモデルの値が表示されます。

私はそれを使って回避しましたModelState.Clear()が、なぜ/どのように機能したのかを知る必要があるので、私は盲目的にそれをしていません。

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、それが再びユーザーにモデルを与えることのポイントは何でしょう、古いデータをキャッシュしたい場合:私は同じ問題が、おかげでたくさんの仲間だった@
deadManN

回答:


135

MVCのバグだと思います。今日、この問題に何時間も苦労しました。

これを考えると:

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

ビューは、変更を無視して、元のモデルでレンダリングされます。だから、同じモデルを使うのは嫌いかもしれないので、次のように試してみました。

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

それでも、ビューは元のモデルでレンダリングされます。奇妙なことに、ビューにブレークポイントを設定してモデルを調べると、値が変更されています。しかし、応答ストリームには古い値があります。

最終的に私はあなたがしたのと同じ回避策を発見しました:

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

期待どおりに動作します。

これは「機能」ではないと思いますか?


33
ちょうどあなたとほぼ同じことをやった。ただし、これはバグではありません。それは仕様によるものです:バグですか?EditorForとDisplayForが同じ値表示せずASP.NET MVCのHTMLヘルパーが誤った値をレンダリングする
Metro Smurf

8
男、私はすでにそれとの戦いに2時間費やしました。この回答を投稿していただきありがとうございます。
Andrey Agibalov

37
これはまだ真実であり、私を含む多くの人々はこれのために多くの時間を失っています。バグまたは設計により、私は気にしません、それは「予想外」です。
Proviste

7
@Provisteに同意します。この「機能」が将来削除されることを願っています
Ben

8
私はこれに4時間費やしました。醜い。
ブライアンマッケイ2013

46

更新:

  • これはバグではありません。
  • View()POSTアクションからの戻りを停止してください。代わりにPRGを使用し、アクションが成功した場合はGETにリダイレクトします。
  • POSTアクションから返す場合View()、フォーム検証のためにそれを行い、組み込みヘルパーを使用してMVCを設計する方法でそれを行います。このようにすれば、使用する必要はありません.Clear()
  • このアクションを使用してSPAの ajaxを返す場合は、Web APIコントローラをModelState使用してください。とにかくそれを使用するべきではないので、忘れてください。

古い答え:

MVCのModelStateは、主にモデルオブジェクトの状態を、そのオブジェクトが有効かどうかに関係して主に記述するために使用されます。このチュートリアルはたくさん説明するべきです。

通常、ModelStateはMVCエンジンによって維持されるため、クリアする必要はありません。手動でクリアすると、MVC検証のベストプラクティスに準拠しようとすると、望ましくない結果が生じる可能性があります。

タイトルのデフォルト値を設定しようとしているようです。これは、モデルオブジェクトがインスタンス化されたときに(ドメインレイヤーのどこかまたはオブジェクト自体-パラメーターなしのctor)、最初にまたはajaxまたは何かを介してクライアントで完全にページに到達するようなgetアクションで実行する必要がありますこれにより、ユーザーが入力したように表示され、投稿されたフォームコレクションが返されます。(POSTアクション//編集で)フォームコレクションの受信時にこの値を追加する方法によって、この奇妙な動作が発生し、.Clear() 外観が機能する場合があります。私を信じて-あなたはクリアを使用したくない。他のアイデアの1つを試してください。


1
サービスレイヤーを少し考え直すのに役立ちます(うめき声だけどthx)が、ネット上の多くのものと同様に、検証にModelStateを使用するという視点に大きく依存しています。
Grok氏、

質問に詳細情報を追加して、なぜ私がModelState.Clear()に特に興味を持っているのか、およびクエリの理由を説明しました
Mr Grok

5
私は本当にこの引数を購入していない停止 [HttpPost]関数からビュー(...)を返します。ajax経由でコンテンツをPOSTし、結果のPartialViewでドキュメントを更新する場合、MVC ModelStateが正しくないことが示されています。私が見つけた唯一の回避策は、コントローラーメソッドでそれをクリアすることです。
Aaron Hudon、2016年

@AaronHudon PRGはかなり確立されています。
Matt Kocaj 16年

AJAX呼び出しでPOSTを実行する場合、GETアクションにリダイレクトして、OPが望むように、モデルで満たされたビューをすべて非同期で返すことができますか?
MyiEye

17

個々のフィールドの値をクリアしたい場合は、次の方法が便利です。

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

注: 「キー」を、リセットするフィールドの名前に変更します。


これがなぜ私のために異なって機能したのか(おそらくMVC4)わかりませんか?しかし、後でmodel.Key = ""も実行する必要がありました。両方の行が必要です。
TTT 2013年

削除コメント@PeterGluckについてお褒めしたいと思います。完全なmodelstateをクリアするよりも優れています(保持したいフィールドでエラーが発生したため)。
Tjab

6

さて、ModelStateは基本的に、検証に関してモデルの現在の状態を保持します。

ModelErrorCollection:モデルが値をバインドしようとしたときのエラーを表します。例。

TryUpdateModel();
UpdateModel();

またはActionResultのパラメータのように

public ActionResult Create(Person person)

ValueProviderResult:モデルへのバインドの試行に関する詳細を保持します。例。AttemptedValue、Culture、RawValue

Clear()メソッドは、予期しない結果になる可能性があるため、注意して使用する必要があります。また、AttemptedValueなどのModelStateのいくつかの優れたプロパティが失われます。これは、エラーの場合にフォームの値を再入力するためにバックグラウンドでMVCによって使用されます。

ModelState["a"].Value.AttemptedValue

1
うーん...それは私がそれのルックスによって問題を得ているところかもしれません。Model.SeoTitleプロパティの値を調べましたが、変更されましたが、試行された値は変更されていません。エラーがない場合でも、ページにエラーがあるかのように値を固定しているように見えます(ModelState辞書を確認し、エラーはありません)。
Grok氏、

6

送信されたフォームのモデルを更新したいインスタンスがあり、パフォーマンス上の理由で「アクションにリダイレクト」したくありませんでした。非表示フィールドの以前の値が私の更新されたモデルで保持されていた-あらゆる種類の問題を引き起こしています!。

数行のコードにより、すぐに削除したい(検証後に)ModelState内の要素が特定されたため、新しい値は次の形式で使用されました。

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

5

私たちの多くはこれに噛まれたようで、これが発生する理由は理にかなっていますが、ModelStateではなく、Modelの値が表示されることを確認する方法が必要でした。

一部のユーザーはを提案ModelState.Remove(string key)していkeyますが、特にネストされたモデルの場合はどうあるべきかが明確ではありません。これを支援するために私が思いついた方法がいくつかあります。

このRemoveStateForメソッドはModelStateDictionary、必要なプロパティの、モデル、および式を受け取り、それを削除します。HiddenForModelビューで使用して、最初にModelStateエントリを削除することにより、モデルの値のみを使用して非表示の入力フィールドを作成できます。(これは他のヘルパー拡張メソッドのために簡単に拡張できます)。

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

次のようなコントローラーから呼び出します。

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

またはこのようなビューから:

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

System.Web.Mvc.ExpressionHelperModelStateプロパティの名前を取得するために使用します。


1
非常に素晴らしい!ExpressionHelper機能のために、このタブを維持します。
Gerard ONeill、

4

値が完全に検証されない場合、値を更新またはリセットする必要があり、この問題が発生しました。

簡単な答えは、ModelState.Removeです。問題があります。ヘルパーを使用している場合は、名前がわからないためです(命名規則に固執しない限り)。カスタムヘルパーとコントローラーの両方が名前を取得するために使用できる関数を作成しない限り、

この機能はヘルパーのオプションとして実装されているはずですが、デフォルトではこれは行われませが、受け入れられない入力を再表示したい場合は、そのように言うことができます。

しかし、少なくとも私は今問題を理解しています;)。


これを正確に行う必要がありました。以下に投稿した私のメソッドを参照してくださいRemove()。これにより、正しいキーが役立ちました。
Tobias J

0

最後にそれを得た。登録されておらず、これを行う私のカスタムModelBinder:

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

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

したがって、デフォルトのモデルバインディングが行っていたことが問題を引き起こしていたに違いありません。何なのかはわかりませんが、カスタムモデルバインダーが登録されているため、問題は少なくとも修正されています。


さて、私はカスタムModelBinderの経験がありません。デフォルトのものはこれまでのところ私のニーズに適合しています=)。
JOBG

0

一般的に、フレームワークの標準的なプラクティスとの闘いを見つけたときは、アプローチを再検討するときです。この場合、ModelStateの動作。たとえば、POST後にモデルの状態が必要ない場合は、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);
}

文化のコメントに答えるように編集:

多文化MVCアプリケーションを処理するために使用するものを以下に示します。まず、ルートハンドラーのサブクラス:

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
}

そして、これが私がルートを配線する方法です。ルートを作成した後、サブエージェント(example.com/subagent1、example.com/subagent2など)を追加し、次にカルチャコードを追加します。必要なのがカルチャだけの場合は、ルートハンドラーとルートからサブエージェントを削除するだけです。

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

    }

POST REDIRECTプラクティスを提案しているのは正解です。実際、私はほとんどすべてのポストアクションに対してこれを行っています。ただし、非常に特別なニーズがありました。ページの上部にフィルターフォームがあり、最初はgetで送信しました。しかし、日付フィールドがバインドされていないという問題に遭遇し、GETリクエストがカルチャを運んでいないことを発見しました(私はアプリにフランスを使用しています)、日付を正常にバインドするにはリクエストをPOSTに切り替える必要がありました。そして、この問題が来て、私は少しは...彼女にこだわっ
Souhaieb Besbes

@SouhaiebBesbes私がどのように文化を扱っているかを示す私の最新情報をご覧ください。
B2K 2016年

@SouhaiebBesbesはおそらく、TempDataにカルチャを格納する方が少し簡単です。stackoverflow.com/questions/12422930/…を
B2K

0

まあ、これは私のRazorページで機能するようで、.csファイルへの往復さえしませんでした。これは古いhtmlの方法です。役に立つかもしれません。

<input type="reset" value="Reset">
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.