ASP.NET MVC-RedirectToAction全体でModelStateエラーを保持する方法


91

次の2つのアクションメソッドがあります(質問では簡略化しています)。

[HttpGet]
public ActionResult Create(string uniqueUri)
{
   // get some stuff based on uniqueuri, set in ViewData.  
   return View();
}

[HttpPost]
public ActionResult Create(Review review)
{
   // validate review
   if (validatedOk)
   {
      return RedirectToAction("Details", new { postId = review.PostId});
   }  
   else
   {
      ModelState.AddModelError("ReviewErrors", "some error occured");
      return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
   }   
}

したがって、検証に合格すると、別のページにリダイレクトされます(確認)。

エラーが発生した場合、エラーと同じページを表示する必要があります。

実行return View()するとエラーが表示されますが、return RedirectToAction(上記のように)実行すると、モデルエラーが失われます。

私はこの問題に驚いていない、あなたたちがこれをどのように処理するのかと思っているだけですか?

もちろん、リダイレクトの代わりに同じビューを返すだけでもかまいませんが、「Create」メソッドにロジックがあり、ビューデータを入力します。これを複製する必要があります。

助言がありますか?


10
検証エラーにPost-Redirect-Getパターンを使用しないことで、この問題を解決しています。私はView()を使用します。一連のフープをジャンプする代わりにそれを行うことは完全に有効です-そしてブラウザの履歴で混乱をリダイレクトします。
ジミーボガード

2
そして、@ JimmyBogardが言ったことに加えて、ViewData Createを生成するメソッドのロジックを抽出し、CreateGETメソッドで呼び出し、CreatePOSTメソッドの失敗した検証ブランチでも呼び出します。
Russ Cam、

1
同意し、問題を回避することはそれを解決する1つの方法です。私は自分のCreateビューにデータを取り込むためのロジックをいくつか持っています。それを、とfailのpopulateStuff両方で呼び出すいくつかのメソッドに入れます。GETPOST
Francois Joly

12
@JimmyBogard同意しません。アクションに投稿してから、ビューに戻ると、問題が発生します。ユーザーが更新を押すと、投稿を再度開始したいという警告が表示されます。
マフィンマン

回答:


50

あなたは、同じインスタンスを持っている必要がありReview、あなたの上HttpGetのアクション。これを行うReview reviewには、HttpPostアクションのオブジェクトを一時変数に保存し、アクションで復元しますHttpGet

[HttpGet]
public ActionResult Create(string uniqueUri)
{
   //Restore
   Review review = TempData["Review"] as Review;            

   // get some stuff based on uniqueuri, set in ViewData.  
   return View(review);
}
[HttpPost]
public ActionResult Create(Review review)
{
   //Save your object
   TempData["Review"] = review;

   // validate review
   if (validatedOk)
   {
      return RedirectToAction("Details", new { postId = review.PostId});
   }  
   else
   {
      ModelState.AddModelError("ReviewErrors", "some error occured");
      return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
   }   
}

HttpGetアクションの最初の実行後にブラウザが更新された場合でもこれを機能させるには、次のようにします。

  Review review = TempData["Review"] as Review;  
  TempData["Review"] = review;

それ以外の場合、reviewにはデータがないため、更新ボタンオブジェクトは空になりますTempData["Review"]


2
優れた。そして、更新の問題について言及するための大きな+1。これは最も完全な答えなので、私はそれを受け入れます。:)
RPM1984、2011年

8
これはタイトルの質問には実際には答えません。ModelStateは保持されず、入力HtmlHelpersがユーザーエントリを保持しないなどの影響があります。これはほぼ回避策です。
John Farrell、2011年

私は@Wimが彼の答えで提案したことをやった。
RPM1984、2011年

17
@jfar、同意します。この回答は機能せず、ModelStateを永続化しません。ただし、それを変更してのように実行しTempData["ModelState"] = ModelState; 、で復元するとModelState.Merge((ModelStateDictionary)TempData["ModelState"]);、機能します
asgeo1

1
return Create(uniqueUri)POSTで検証が失敗したときだけではありませんか?ModelState値はビューに渡されるViewModelよりも優先されるため、ポストされたデータはそのまま残ります。
ajbeaven 2013年

83

私は自分でこの問題を解決しなければならず、この質問に出くわしました。

いくつかの回答は(TempDataを使用して)役立ちますが、実際の質問には実際には回答しません。

私が見つけた最高のアドバイスはこのブログ投稿にありました:

http://www.jefclaes.be/2012/06/persisting-model-state-when-using-prg.html

基本的に、TempDataを使用してModelStateオブジェクトを保存および復元します。ただし、これを属性に抽象化すると、よりクリーンになります。

例えば

public class SetTempDataModelStateAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        base.OnActionExecuted(filterContext);         
        filterContext.Controller.TempData["ModelState"] = 
           filterContext.Controller.ViewData.ModelState;
    }
}

public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);
        if (filterContext.Controller.TempData.ContainsKey("ModelState"))
        {
            filterContext.Controller.ViewData.ModelState.Merge(
                (ModelStateDictionary)filterContext.Controller.TempData["ModelState"]);
        }
    }
}

次に、例のように、ModelStateを次のように保存/復元できます。

[HttpGet]
[RestoreModelStateFromTempData]
public ActionResult Create(string uniqueUri)
{
    // get some stuff based on uniqueuri, set in ViewData.  
    return View();
}

[HttpPost]
[SetTempDataModelState]
public ActionResult Create(Review review)
{
    // validate review
    if (validatedOk)
    {
        return RedirectToAction("Details", new { postId = review.PostId});
    }  
    else
    {
        ModelState.AddModelError("ReviewErrors", "some error occured");
        return RedirectToAction("Create", new { uniqueUri = Request.RequestContext.RouteData.Values["uniqueUri"]});
    }   
}

(bigbが示唆するように)TempDataでもモデルを渡したい場合は、それも可能です。


ありがとうございました。私たちはあなたのアプローチに似たものを実装しました。gist.github.com/ferventcoder/4735084
ferventcoder、

すばらしい答えです。ありがとう。
Mark Vickery 2014年

3
このソリューションが私がstackoverflowを使用する理由です。ありがとう!
jugg1es 2014年

@ asgeo1 -優れたソリューションが、私は部分的なビューを繰り返すと組み合わせて使用して問題に走った、私はここに質問を投稿:stackoverflow.com/questions/28372330/...
ジョシュ

シンプルなソリューションを採用し、MVCの精神で非常にエレガントにする素敵な例。非常に素晴らしい!
AHowgego

7

「Create」メソッドのロジックを使用してプライベート関数を作成し、GetメソッドとPostメソッドの両方からこのメソッドを呼び出して、単にView()を返さないのはなぜですか。


これは実際に私がやったことです-あなたは私の心を読みました。+1 :)
RPM1984、2011年

1
これも私が行うことであり、プライベート関数を使用する代わりに、POSTメソッドにエラー時にGETメソッドを呼び出させるだけです(つまり、return Create(new { uniqueUri = ... });ロジックは(呼び出しのようにRedirectToAction)DRYのままですが、次のようなリダイレクトによって実行される問題はありません。 ModelStateを失う
Daniel Liuzzi

1
@DanielLiuzzi:そのようにしてもURLは変更されません。したがって、「/ controller / create /」のようなURLで終わります。
SkorunkaFrantišek12年

@SkorunkaFrantišekそれがまさにポイントです。質問は、エラーが発生した場合、エラーのある同じページを表示する必要があると述べています。このコンテキストでは、同じページが表示されてもURLが変更されないことは完全に許容できます(そしてIMOが望ましい)。また、このアプローチの利点の1つは、問題のエラーが検証エラーではなくシステムエラー(DBタイムアウトなど)の場合に、ユーザーがページを更新してフォームを再送信できることです。
Daniel Liuzzi

4

私は使うことができた TempData["Errors"]

TempDataは、データを1回保存するアクション間で渡されます。


4

ビューを返し、アクションの属性による重複を避けることをお勧めします。以下は、データを表示するために入力する例です。createメソッドのロジックでも同様のことができます。

public class GetStuffBasedOnUniqueUriAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var filter = new GetStuffBasedOnUniqueUriFilter();

        filter.OnActionExecuting(filterContext);
    }
}


public class GetStuffBasedOnUniqueUriFilter : IActionFilter
{
    #region IActionFilter Members

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {

    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.Controller.ViewData["somekey"] = filterContext.RouteData.Values["uniqueUri"];
    }

    #endregion
}

次に例を示します。

[HttpGet, GetStuffBasedOnUniqueUri]
public ActionResult Create()
{
    return View();
}

[HttpPost, GetStuffBasedOnUniqueUri]
public ActionResult Create(Review review)
{
    // validate review
    if (validatedOk)
    {
        return RedirectToAction("Details", new { postId = review.PostId });
    }

    ModelState.AddModelError("ReviewErrors", "some error occured");
    return View(review);
}

これはどのように悪い考えですか?両方のアクションがViewDataにロードするために属性を使用できるため、属性は別のアクションを使用する必要性を回避すると思います。
CRice

1
Post / Redirect / Getパターンを見てください:en.wikipedia.org/wiki/Post/Redirect/Get
DreamSonic

2
これは通常、モデルの検証が満たされた後に使用され、更新時に同じフォームにそれ以上投稿されないようにします。ただし、フォームに問題がある場合は、修正して再投稿する必要があります。この質問では、モデルエラーの処理について扱います。
CRice

フィルターは、アクションの再利用可能なコード用で、特にViewDataに物を置く場合に役立ちます。TempDataは単なる回避策です。
CRice

1
@ppumkinは、ビューサーバー側の再構築に苦労しないように、ajaxで投稿してみてください。
CRice 2015

2

一時データにモデルの状態を追加するメソッドがあります。次に、一時データにエラーがないかどうかをチェックするメソッドをベースコントローラに配置します。それらがある場合は、ModelStateに追加し直します。


1

PRGパターンを使用しているため、私のシナリオは少し複雑になっているため、ViewModel( "SummaryVM")はTempDataにあり、Summary画面に表示されます。このページには、別のアクションに情報をPOSTするための小さなフォームがあります。複雑さは、ユーザーがこのページのSummaryVMの一部のフィールドを編集する必要があるために生じています。

Summary.cshtmlには、作成するModelStateエラーをキャッチする検証サマリーがあります。

@Html.ValidationSummary()

私のフォームは、Summary()のHttpPostアクションにPOSTする必要があります。編集済みのフィールドを表す非常に小さなViewModelがもう1つあります。モデルバインディングを使用すると、これらを取得できます。

新しいフォーム:

@using (Html.BeginForm("Summary", "MyController", FormMethod.Post))
{
    @Html.Hidden("TelNo") @* // Javascript to update this *@

そしてアクション...

[HttpPost]
public ActionResult Summary(EditedItemsVM vm)

ここで検証を行い、いくつかの不正な入力を検出したため、エラーのある[概要]ページに戻る必要があります。これには、リダイレクトを存続させるTempDataを使用します。データに問題がない場合は、SummaryVMオブジェクトをコピーで置き換えます(ただし、編集されたフィールドはもちろん変更します)。次に、RedirectToAction( "NextAction");を実行します。

// Telephone number wasn't in the right format
List<string> listOfErrors = new List<string>();
listOfErrors.Add("Telephone Number was not in the correct format. Value supplied was: " + vm.TelNo);
TempData["SummaryEditedErrors"] = listOfErrors;
return RedirectToAction("Summary");

これがすべて始まるサマリーコントローラーアクションは、tempdata内のエラーを探し、それらをmodelstateに追加します。

[HttpGet]
[OutputCache(Duration = 0)]
public ActionResult Summary()
{
    // setup, including retrieval of the viewmodel from TempData...


    // And finally if we are coming back to this after a failed attempt to edit some of the fields on the page,
    // load the errors stored from TempData.
        List<string> editErrors = new List<string>();
        object errData = TempData["SummaryEditedErrors"];
        if (errData != null)
        {
            editErrors = (List<string>)errData;
            foreach(string err in editErrors)
            {
                // ValidationSummary() will see these
                ModelState.AddModelError("", err);
            }
        }

1

マイクロソフトはTempDataに複雑なデータ型を格納する機能を削除したため、以前の回答は機能しなくなりました。文字列のような単純な型のみを保存できます。期待どおりに動作するように@ asgeo1による回答を変更しました。

public class SetTempDataModelStateAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        base.OnActionExecuted(filterContext);

        var controller = filterContext.Controller as Controller;
        var modelState = controller?.ViewData.ModelState;
        if (modelState != null)
        {
            var listError = modelState.Where(x => x.Value.Errors.Any())
                .ToDictionary(m => m.Key, m => m.Value.Errors
                .Select(s => s.ErrorMessage)
                .FirstOrDefault(s => s != null));
            controller.TempData["KEY HERE"] = JsonConvert.SerializeObject(listError);
        }
    }
}


public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);

        var controller = filterContext.Controller as Controller;
        var tempData = controller?.TempData?.Keys;
        if (controller != null && tempData != null)
        {
            if (tempData.Contains("KEY HERE"))
            {
                var modelStateString = controller.TempData["KEY HERE"].ToString();
                var listError = JsonConvert.DeserializeObject<Dictionary<string, string>>(modelStateString);
                var modelState = new ModelStateDictionary();
                foreach (var item in listError)
                {
                    modelState.AddModelError(item.Key, item.Value ?? "");
                }

                controller.ViewData.ModelState.Merge(modelState);
            }
        }
    }
}

ここから、必要に応じてコントローラーメソッドに必要なデータアノテーションを追加するだけです。

[RestoreModelStateFromTempDataAttribute]
[HttpGet]
public async Task<IActionResult> MethodName()
{
}


[SetTempDataModelStateAttribute]
[HttpPost]
public async Task<IActionResult> MethodName()
{
    ModelState.AddModelError("KEY HERE", "ERROR HERE");
}

完全に動作します!。コードを貼り付けるときの小さなブラケットエラーを修正するために回答を編集しました。
VDWWD

0

デフォルト値を入力するメソッドをViewModelに追加することを好みます。

public class RegisterViewModel
{
    public string FirstName { get; set; }
    public IList<Gender> Genders { get; set; }
    //Some other properties here ....
    //...
    //...

    ViewModelType PopulateDefaultViewData()
    {
        this.FirstName = "No body";
        this.Genders = new List<Gender>()
        {
            Gender.Male,
            Gender.Female
        };

        //Maybe other assinments here for other properties...
    }
}

次に、次のような元のデータが必要になったときに呼び出します。

    [HttpGet]
    public async Task<IActionResult> Register()
    {
        var vm = new RegisterViewModel().PopulateDefaultViewValues();
        return View(vm);
    }

    [HttpPost]
    public async Task<IActionResult> Register(RegisterViewModel vm)
    {
        if (!ModelState.IsValid)
        {
            return View(vm.PopulateDefaultViewValues());
        }

        var user = await userService.RegisterAsync(
            email: vm.Email,
            password: vm.Password,
            firstName: vm.FirstName,
            lastName: vm.LastName,
            gender: vm.Gender,
            birthdate: vm.Birthdate);

        return Json("Registered successfully!");
    }

0

ここではサンプルコードのみを示しています。viewModelでは、「ModelStateDictionary」タイプのプロパティを1つ追加できます。

public ModelStateDictionary ModelStateErrors { get; set; }

そしてあなたのPOSTアクションの方法では、あなたは直接のようにコードを書くことができます

model.ModelStateErrors = ModelState; 

そして、このモデルを以下のようにTempdataに割り当てます

TempData["Model"] = model;

他のコントローラーのアクションメソッドにリダイレクトする場合は、コントローラーでTempdata値を読み取る必要があります。

if (TempData["Model"] != null)
{
    viewModel = TempData["Model"] as ViewModel; //Your viewmodel class Type
    if(viewModel.ModelStateErrors != null && viewModel.ModelStateErrors.Count>0)
    {
        this.ViewData.ModelState.Merge(viewModel.ModelStateErrors);
    }
}

それでおしまい。このためのアクションフィルターを作成する必要はありません。別のコントローラーの別のビューにモデル状態エラーを取得する場合、これは上記のコードと同じくらい簡単です。

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