web-dev-qa-db-ja.com

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

次の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_を実行すると(上記のように)、モデルエラーが失われます。

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

もちろん、リダイレクトの代わりに同じビューを返すこともできますが、「作成」メソッドには、複製する必要があるビューデータを取り込むロジックがあります。

助言がありますか?

83
RPM1984

ReviewアクションにHttpGetの同じインスタンスが必要です。これを行うには、HttpPostアクションの一時変数にオブジェクトReview reviewを保存し、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"]にデータがないため、空になります。

49
Kuncevič

今日私はこの問題を自分で解決しなければならなかったので、この質問に出会いました。

いくつかの答えは(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"]});
    }   
}

また、TempDataでモデルを渡す(bigbが推奨するように)場合も、それを行うことができます。

73
asgeo1

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

7
Wim

TempData["Errors"]を使用できます

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

4
rob waminal

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

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

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

2
nick

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

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

@Html.ValidationSummary()

私のフォームは現在、Summary()のHttpPostアクションにPOSTする必要があります。編集済みフィールドを表す別の非常に小さなViewModelがあり、modelbindingがこれらを取得します。

新しいフォーム:

@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のエラーを探し、それらをモデル状態に追加します。

[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
VictorySaber

私は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

Microsoftは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");
    }
0
Alex Marchant