web-dev-qa-db-fra.com

ASP.NET MVC - Comment préserver les erreurs ModelState dans RedirectToAction?

J'ai les deux méthodes d'action suivantes (simplifiées pour la question):

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

Donc, si la validation réussit, je redirige vers une autre page (confirmation).

Si une erreur survient, je dois afficher la même page avec l'erreur.

Si je fais return View(), l'erreur est affichée, mais si je fais return RedirectToAction (comme ci-dessus), il perd les erreurs de modèle.

Je ne suis pas surpris par le problème, je me demande simplement comment vous gérez cela?

Je pourrais bien sûr simplement renvoyer la même vue au lieu de la redirection, mais j’ai une logique dans la méthode "Créer" qui remplit les données de la vue, que je devrais dupliquer.

Aucune suggestion?

76
RPM1984

Vous devez avoir la même instance de Review sur votre action HttpGet . Pour ce faire, vous devez enregistrer un objet Review review dans une variable temporaire sur votre action HttpPost, puis le restaurer sur une action 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"]});
   }   
}

Aussi, je conseillerais, si vous voulez le faire fonctionner aussi quand le bouton de rafraîchissement du navigateur est appuyé après l'action HttpGet exécutée la première fois, vous pouvez aller comme ça 

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

Sinon, sur le bouton de rafraîchissement, l'objet review sera vide car il n'y aurait aucune donnée dans TempData["Review"].

45
Kuncevič

Je devais résoudre ce problème moi-même aujourd'hui et je suis tombé sur cette question.

Certaines des réponses sont utiles (avec TempData), mais ne répondent pas vraiment à la question posée.

Le meilleur conseil que j'ai trouvé était sur ce blog:

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

Fondamentalement, utilisez TempData pour enregistrer et restaurer l'objet ModelState. Cependant, c'est beaucoup plus propre si vous abstenez ceci en attributs.

Par exemple.

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

Ensuite, selon votre exemple, vous pouvez enregistrer/restaurer le ModelState comme suit:

[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 vous souhaitez également transmettre le modèle dans TempData (comme suggéré par Bigb), vous pouvez toujours le faire également.

69
asgeo1

Pourquoi ne pas créer une fonction privée avec la logique de la méthode "Create" et appeler cette méthode à partir des méthodes Get et Post et ne renvoyer que View ().

7
Wim

Je pourrais utiliser TempData["Errors"]

TempData sont passés à travers les actions en préservant les données 1 fois.

4
rob waminal

Je vous suggère de retourner la vue et d'éviter les doublons via un attribut de l'action. Voici un exemple de remplissage pour afficher des données. Vous pouvez faire quelque chose de similaire avec votre logique de méthode 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
}

Voici un exemple:

[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

J'ai une méthode qui ajoute l'état du modèle aux données temporaires. J'ai alors une méthode dans mon contrôleur de base qui vérifie les données temporaires pour toutes les erreurs. S'il en a, il les rajoute à ModelState.

2
nick

Mon scénario est un peu plus compliqué car j'utilise le modèle PRG. Mon ViewModel ("SummaryVM") est donc dans TempData et mon écran Résumé l'affiche. Il y a un petit formulaire sur cette page pour POST quelques informations à une autre action . La complication est due à l'obligation pour l'utilisateur de modifier certains champs dans SummaryVM sur cette page.

Summary.cshtml contient le résumé de validation qui interceptera les erreurs ModelState que nous allons créer.

@Html.ValidationSummary()

Mon formulaire a maintenant besoin de POST pour une action HttpPost pour Summary (). J'ai un autre très petit ViewModel pour représenter les champs édités, et modelbinding me les transmettra.

Le nouveau formulaire:

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

et l'action ...

[HttpPost]
public ActionResult Summary(EditedItemsVM vm)

Ici, je fais une validation et je détecte une mauvaise entrée, donc je dois retourner à la page Résumé avec les erreurs. Pour cela, j'utilise TempData, qui survivra à une redirection . S'il n'y a pas de problème avec les données, je remplace l'objet SummaryVM par une copie (mais les champs modifiés ont bien sûr été modifiés), puis je fais une 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");

L'action du contrôleur récapitulatif, où tout cela commence, recherche les erreurs éventuelles dans les données temporaires et les ajoute à l'état de la modélisation.

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

Je préfère ajouter une méthode à mon ViewModel qui renseigne les valeurs par défaut: 

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

Ensuite, je l’appelle chaque fois que j’ai besoin des données originales comme ceci:

    [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
Mohammed Noureldin

Microsoft a supprimé la possibilité de stocker des types de données complexes dans TempData; par conséquent, les réponses précédentes ne fonctionnent plus; vous ne pouvez stocker que des types simples tels que des chaînes. J'ai modifié la réponse de @ asgeo1 pour qu'elle fonctionne comme prévu.

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

À partir de là, vous pouvez simplement ajouter les annotations de données requises sur une méthode de contrôleur selon vos besoins.

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

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