ASP.NET MVC - ¿Cómo Preservar los Errores de ModelState En RedirectToAction?


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, redirijo a otra página (confirmación).

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

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

No me sorprende el problema, solo me pregunto cómo ustedes manejan esto?

Podría, por supuesto, 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ía que duplicar.

Alguna sugerencia?

Author: RPM1984, 2011-01-10

9 answers

Necesitas tener la misma instancia de Review en tu acción HttpGet. Para ello, debe guardar un objeto Review review en variable temporal 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 me gustaría aconsejar, si desea que funcione también cuando el botón de actualización del navegador presionado después de HttpGet acción ejecutada por primera vez, puede ir así

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

De lo contrario, el objeto del botón actualizar review estará vacío porque no habría ningún dato en TempData["Review"].

 43
Author: Kuncevič,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-10 01:39:28

Tuve que resolver este problema hoy mismo, y me encontré con esta pregunta.

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

El mejor consejo que encontré fue en esta entrada del blog:

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

Básicamente, utilice TempData para guardar y restaurar el objeto ModelState. Sin embargo, es mucho más limpio si abstrae esto en atributo.

Por ejemplo

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 en TempData (como sugirió bigb), puede hacerlo también.

 68
Author: asgeo1,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-19 04:30:21

¿Por qué no crear una función privada con la lógica en el método "Create" y llamar a este método desde el método Get y el Post y simplemente hacer return View().

 6
Author: Wim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-10 11:11:43

Podría usar TempData["Errors"]

Los datos temporales se pasan a través de acciones conservando los datos 1 vez.

 4
Author: rob waminal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-10 00:52:42

Le sugiero que devuelva la vista y evite la duplicación a través de un atributo en la acción. Aquí hay un ejemplo de rellenar para ver datos. Podrías hacer algo similar con la lógica del método 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
}

Aquí hay un ejemplo:

[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
Author: CRice,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-04 23:38:37

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

 2
Author: nick,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-10 01:27:14

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

Resumen.cshtml tiene el resumen de validación que capturará los errores de ModelState que crearemos.

@Html.ValidationSummary()

Mi formulario ahora necesita publicar en un Acción HttpPost para Summary (). Tengo otro ViewModel muy pequeño para representar los campos editados, y modelbinding me los dará.

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 algunas validaciones y detecto algunas entradas incorrectas, por lo que necesito volver a la página de Resumen con los errores. Para ello utilizo TempData, que sobrevivirá a una redirección. Si no hay ningún problema con los datos, sustituyo el objeto SummaryVM por una copia (pero con los campos editados cambiado por supuesto) luego haga 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 Summary controller, donde comienza todo esto, busca cualquier error en los tempdata y los agrega al 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);
            }
        }
 0
Author: VictorySaber,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-10 17:22:30

Prefiero agregar un método a mi ViewModel que rellene 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...
    }
}

Entonces 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!");
    }
 0
Author: Mohammed Noureldin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-23 00:13:35

Microsoft eliminó la capacidad de almacenar tipos de datos complejos en TempData, por lo tanto, las respuestas anteriores ya no funcionan; solo puede almacenar tipos simples como cadenas. He alterado la respuesta de @asgeo1 para que funcione como se esperaba.

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

Desde aquí, simplemente puede agregar la anotación de datos requerida en un método de controlador según sea necesario.

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

[SetTempDataModelStateAttribute]
    [HttpPost]
    public async Task<IActionResult> MethodName()
    {
            ModelState.AddModelError("KEY HERE", "ERROR HERE");
    }
 0
Author: Alex Marchant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-07 16:30:40