prg mvc asp.net-mvc error-handling modelstate redirecttoaction http-redirect

prg - ASP.NET MVC: ¿cómo preservar los errores de ModelState en RedirectToAction?



prg mvc (8)

Tengo los siguientes dos métodos de acción (simplificados para la pregunta):

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

Entonces, si la validación pasa, me redirige a otra página (confirmación).

Si ocurre un error, necesito mostrar la misma página con el error.

Si return View() , se muestra el error, pero si return RedirectToAction (como se return RedirectToAction anteriormente), pierde los errores del Modelo.

No estoy sorprendido por el problema, solo me pregunto cómo ustedes manejan esto?

Por supuesto, podría devolver la misma Vista en lugar de la redirección, pero tengo lógica en el método "Crear" que rellena los datos de la vista, que tendré que duplicar.

¿Alguna sugerencia?


¿Por qué no crear una función privada con la lógica en el método "Crear" y llamar a este método desde el método Obtener y Publicar y simplemente devolver View ().


Hoy mismo tuve que resolver este problema y me encontré con esta pregunta.

Algunas de las respuestas son útiles (usando TempData), pero realmente no responden la pregunta en cuestión.

El mejor consejo que encontré fue en esta publicación de blog:

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

Básicamente, use TempData para guardar y restaurar el objeto ModelState. Sin embargo, es mucho más limpio si abstraes esto en atributos.

P.ej

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

Luego, según su ejemplo, podría guardar / restaurar el ModelState de la siguiente manera:

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

Si también desea pasar el modelo junto con TempData (como sugirió bigb), puede hacerlo también.


Mi escenario es un poco más complicado ya que estoy usando el patrón PRG para que mi ViewModel ("SummaryVM") esté en TempData, y mi pantalla de resumen lo muestra. Hay un pequeño formulario en esta página para enviar información a otra Acción. La complicación proviene de un requisito para que el usuario edite algunos campos en SummaryVM en esta página.

Summary.cshtml tiene el resumen de validación que detectará los errores de ModelState que crearemos.

@Html.ValidationSummary()

Mi formulario ahora debe enviar a una acción HttpPost para Summary (). Tengo otro ViewModel muy pequeño para representar los campos editados, y la vinculación del modelo me los proporcionará.

La nueva forma:

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

y la acción ...

[HttpPost] public ActionResult Summary(EditedItemsVM vm)

Aquí hago algo de validación y detecto algunos datos erróneos, por lo que debo volver a la página de Resumen con los errores. Para esto utilizo TempData, que sobrevivirá a una redirección. Si no hay ningún problema con los datos, reemplace el objeto SummaryVM con una copia (pero con los campos editados cambiados, por supuesto) y luego hago un 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");

La acción del controlador de resumen, donde todo comienza, busca cualquier error en los tempdatos y los agrega al estado del modelo.

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


Necesita tener la misma instancia de Review en su acción HttpGet . Para hacer eso, debe guardar un objeto Review review en la variable temp en su acción HttpPost y luego restaurarlo en la acción 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 you 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"]}); } }

También te lo aconsejaría, si quieres que funcione también cuando el botón de actualización del navegador se presiona después de que se HttpGet acción HttpGet primera vez, puedes ir así

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

De lo contrario, la review objeto del botón de actualización estará vacía porque no habrá datos en TempData["Review"] .


Podría usar TempData["Errors"]

Los TempData se pasan a través de acciones que preservan los datos 1 vez.


Prefiero agregar un método a mi ViewModel que rellena los valores predeterminados:

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

Luego lo llamo cuando necesito los datos originales como este:

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


Sugiero que devuelva la vista y evite la duplicación mediante un atributo en la acción. Aquí hay un ejemplo de poblado para ver datos. Podría hacer algo similar con su lógica de método de creación.

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 }

Y para aquellos que simplemente tuvieron dificultades para imaginar cómo funcionaría esto:

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


Tengo un método que agrega estado del modelo a los datos temporales. Luego tengo un método en mi controlador base que comprueba los datos temporales para detectar cualquier error. Si los tiene, los agrega de nuevo a ModelState.