web-dev-qa-db-ja.com

Asp.net MVC ModelState.Clear

Asp.net MVCでのModelStateの役割の簡潔な定義(またはリンク)を教えてください。特に、ModelState.Clear()を呼び出すことが必要または望ましい状況を知る必要があります。

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

「ページ」と呼ばれるコントローラーで編集のアクションがあります。最初にページの詳細を変更するフォームを見ると、すべてが正常に読み込まれます(「MyCmsPage」オブジェクトにバインド)。次に、MyCmsPageオブジェクトのフィールドの1つの値を生成するボタン(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>
112
Mr Grok

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

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

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

132
Tim Scott

更新:

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

古い答え:

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

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

タイトルにデフォルト値を設定しようとしているようです。これは、モデルオブジェクトがインスタンス化されたときに(ドメインレイヤーのどこか、またはオブジェクト自体-パラメーターレスアクター)、1回目またはページ上で完全にクライアントに到達するgetアクションで行われる必要があります(ajaxなどを介して)ユーザーが入力したように表示され、投稿されたフォームコレクションとともに戻ってきます。フォームコレクションの受信(POSTアクション//編集)でこの値を追加するアプローチが、.Clear()あなたのために働くために現れる。私を信じてください-あなたは平文を使いたくありません。他のアイデアを試してください。

41
Matt Kocaj

個々のフィールドの値をクリアしたい場合、次のテクニックが便利だと思いました。

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

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

17
Carl Saunders

申請フォームのモデルを更新したいインスタンスがありましたが、パフォーマンス上の理由で「アクションにリダイレクト」したくありませんでした。非表示フィールドの以前の値は、更新されたモデルで保持されていたため、さまざまな問題が発生していました。

数行のコードがすぐに(検証後に)削除したいModelState内の要素を識別したため、新しい値がフォームで使用されました。

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

ModelStateは基本的に、検証の観点からモデルの現在の状態を保持します。

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

TryUpdateModel();
UpdateModel();

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

public ActionResult Create(Person person)

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

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

ModelState["a"].Value.AttemptedValue
6
JOBG

私たちの多くはこれに噛まれたようです。これが起こる理由は理にかなっていますが、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.ExpressionHelperを使用して、ModelStateプロパティの名前を取得します。

5
Toby J

値が完全に検証されなかった場合、値を更新またはリセットしたいと考え、この問題に遭遇しました。

ModelState.Removeという簡単な答えは、問題があります。ヘルパーを使用している場合、名前を実際には知らないので(命名規則を守らない限り)。おそらく、customヘルパーとコントローラーの両方が名前を取得するために使用できる関数を作成しない限り。

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

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

4
Gerard ONeill

一般的に、フレームワークの標準的な慣行と戦っていることがわかったら、あなたのアプローチを再考する時が来ました。この場合、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()));
            }
        }

    }
0
B2K

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

var mymsPage = new MyCmsPage();

NameValueCollection frm = controllerContext.HttpContext.Request.Form;

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

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

0
Mr Grok

まあ、これは私のRazor Pageで機能するようで、.csファイルへのラウンドトリップもしませんでした。これは古いhtmlの方法です。役に立つかもしれません。

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