NET MVC-如何在 RedirectToAction 中保存模型状态错误?

我有以下两种行动方法(简化为问题) :

[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(如上) ,它丢失的模型错误。

我对这个问题并不感到惊讶,只是想知道你们是怎么处理这件事的?

我当然可以只返回相同的视图,而不是重定向,但我有逻辑在“创建”方法,填充视图数据,我必须复制。

有什么建议吗?

64661 次浏览

I could use TempData["Errors"]

TempData are passed accross actions preserving data 1 time.

You need to have the same instance of Review on your HttpGet action. To do that you should save an object Review review in temp variable on your HttpPost action and then restore it on HttpGet action.

[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"]});
}
}

If you want this to work even if the browser is refreshed after the first execution of the HttpGet action, you could do this:

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

Otherwise on refresh button object review will be empty because there wouldn't be any data in TempData["Review"].

I suggest you return the view, and avoid duplication via an attribute on the action. Here is an example of populating to view data. You could do something similar with your create method logic.

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
}

Here is an example:

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

I have a method that adds model state to temp data. I then have a method in my base controller that checks temp data for any errors. If it has them, it adds them back to ModelState.

Why not create a private function with the logic in the "Create" method and calling this method from both the Get and the Post method and just do return View().

I had to solve this problem today myself, and came across this question.

Some of the answers are useful (using TempData), but don't really answer the question at hand.

The best advice I found was on this blog post:

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

Basically, use TempData to save and restore the ModelState object. However, it's a lot cleaner if you abstract this away into attributes.

E.g.

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

Then as per your example, you could save / restore the ModelState like so:

[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"]});
}
}

If you also want to pass the model along in TempData (as bigb suggested) then you can still do that too.

My scenario is a little bit more complicated as I am using the PRG pattern so my ViewModel ("SummaryVM") is in TempData, and my Summary screen displays it. There is a small form on this page to POST some info to another Action. The complication has come from a requirement for the user to edit some fields in SummaryVM on this page.

Summary.cshtml has the validation summary which will catch ModelState errors that we'll create.

@Html.ValidationSummary()

My form now needs to POST to a HttpPost action for Summary(). I have another very small ViewModel to represent edited fields, and modelbinding will get these to me.

The new form:

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

and the action...

[HttpPost]
public ActionResult Summary(EditedItemsVM vm)

In here I do some validation and I detect some bad input, so I need to return to the Summary page with the errors. For this I use TempData, which will survive a redirection. If there is no issue with the data, I replace the SummaryVM object with a copy (but with the edited fields changed of course) then do a 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");

The Summary controller action, where all this begins, looks for any errors in the tempdata and adds them to the 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);
}
}

I prefer to add a method to my ViewModel which populates the default values:

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...
}
}

Then I call it when ever I need the original data like this:

    [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!");
}

Microsoft removed the ability to store complex data types in TempData, therefore the previous answers no longer work; you can only store simple types like strings. I have altered the answer by @asgeo1 to work as expected.

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

From here, you can simply add the required data annotation on a controller method as needed.

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




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

I am giving just sample code here In your viewModel you can add one property of type "ModelStateDictionary" as

public ModelStateDictionary ModelStateErrors { get; set; }

and in your POST action menthod you can write code directly like

model.ModelStateErrors = ModelState;

and then assign this model to Tempdata like below

TempData["Model"] = model;

and when you redirect to other controller's action method then in controller you have to read the Tempdata value

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

That's it. You don't have to write action filters for this. This is as simple as above code if you want to get Model state errors to another view of another controller.

    public class RestoreModelStateFromTempDataAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = filterContext.Controller as Controller;
if (controller.TempData.ContainsKey("ModelState"))
{
var modelState = ModelStateHelpers.DeserialiseModelState(controller.TempData["ModelState"].ToString());
controller.ViewData.ModelState.Merge(modelState);
}
base.OnActionExecuting(filterContext);
}
}
public class SetTempDataModelStateAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var controller = filterContext.Controller as Controller;
controller.TempData["ModelState"] = ModelStateHelpers.SerialiseModelState(controller.ViewData.ModelState);
base.OnActionExecuted(filterContext);
}
}

When I resolve some problem, I ran into a lot of non-obvious obstacles. I will indicate everything step by step. My comments will partially duplicate the answers from the current branch

  1. Implement two attributes. You must explicitly specify the type for controller (filterContext.Controller as Controller) because default is object type.
  2. Explicitly implement serialization ModelState from this article https://andrewlock.net/post-redirect-get-using-tempdata-in-asp-net-core/
  3. If TempData is empty in destination action when check implementation cache in startup.cs. You need add memoryCache or SqlServerCache or another https://stackoverflow.com/a/41500275/11903993