ASP.NET MVC - Come preservare gli errori ModelState in RedirectToAction?


91

Ho i seguenti due metodi di azione (semplificati per la domanda):

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

Quindi, se la convalida passa, reindirizzo a un'altra pagina (conferma).

Se si verifica un errore, è necessario visualizzare la stessa pagina con l'errore.

Se lo faccio return View(), viene visualizzato l'errore, ma se lo faccio return RedirectToAction(come sopra), perde gli errori del modello.

Non sono sorpreso dal problema, mi chiedo solo come lo gestite?

Ovviamente potrei semplicemente restituire la stessa vista invece del reindirizzamento, ma ho una logica nel metodo "Crea" che popola i dati della vista, che dovrei duplicare.

Eventuali suggerimenti?


10
Risolvo questo problema non utilizzando il pattern Post-Redirect-Get per gli errori di convalida. Uso solo View (). È perfettamente valido farlo invece di saltare attraverso un mucchio di cerchi e reindirizzare pasticci con la cronologia del browser.
Jimmy Bogard

2
E oltre a ciò che ha detto @JimmyBogard, estrai la logica nel Createmetodo che popola ViewData e chiamala nel Createmetodo GET e anche nel ramo di convalida fallita nel Createmetodo POST.
Russ Cam

1
D'accordo, evitare il problema è un modo per risolverlo. Ho un po 'di logica per popolare le cose dal mio punto di Createvista, l'ho semplicemente inserito in un metodo populateStuffche chiamo sia nel GETche nel fail POST.
Francois Joly

12
@JimmyBogard Non sono d'accordo, se pubblichi un'azione e poi ritorni alla visualizzazione ti imbatti nel problema per cui se l'utente preme il pulsante di aggiornamento riceve l'avviso di voler avviare di nuovo il post.
The Muffin Man

Risposte:


50

Devi avere la stessa istanza di Reviewsulla tua HttpGetazione. Per fare ciò dovresti salvare un oggetto Review reviewnella variabile temporanea sulla tua HttpPostazione e poi ripristinarlo in HttpGetazione.

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

Se vuoi che funzioni anche se il browser viene aggiornato dopo la prima esecuzione HttpGetdell'azione, puoi farlo:

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

In caso contrario, l'oggetto del pulsante di aggiornamento reviewsarà vuoto perché non ci sarebbero dati in TempData["Review"].


2
Eccellente. E un grande +1 per aver menzionato il problema dell'aggiornamento. Questa è la risposta più completa quindi la accetterò, grazie mille. :)
RPM1984

8
Questo in realtà non risponde alla domanda nel titolo. ModelState non viene conservato e ha ramificazioni come l'input HtmlHelpers che non preserva la voce dell'utente. Questa è quasi una soluzione alternativa.
John Farrell

Ho finito per fare ciò che @Wim ha suggerito nella sua risposta.
RPM1984,

17
@jfar, sono d'accordo, questa risposta non funziona e non persiste ModelState. Tuttavia, se lo modifichi in modo che faccia qualcosa di simile TempData["ModelState"] = ModelState; e ripristini con ModelState.Merge((ModelStateDictionary)TempData["ModelState"]);, allora funzionerebbe
asgeo1

1
Non potresti solo return Create(uniqueUri)quando la convalida fallisce sul POST? Poiché i valori ModelState hanno la precedenza sul ViewModel passato alla visualizzazione, i dati pubblicati dovrebbero comunque rimanere.
ajbeaven

83

Oggi ho dovuto risolvere questo problema da solo e mi sono imbattuto in questa domanda.

Alcune delle risposte sono utili (utilizzando TempData), ma non rispondono realmente alla domanda in questione.

Il miglior consiglio che ho trovato è stato su questo post del blog:

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

Fondamentalmente, usa TempData per salvare e ripristinare l'oggetto ModelState. Tuttavia, è molto più pulito se lo astratti in attributi.

Per esempio

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

Quindi, come nel tuo esempio, puoi salvare / ripristinare ModelState in questo modo:

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

Se vuoi anche passare il modello in TempData (come suggerito da bigb), puoi comunque farlo anche tu.


Grazie. Abbiamo implementato qualcosa di simile al tuo approccio. gist.github.com/ferventcoder/4735084
ferventcoder

Bella risposta. Grazie.
Mark Vickery

3
Questa soluzione è il motivo per cui utilizzo stackoverflow. Grazie uomo!
jugg1es

@ asgeo1 - ottima soluzione, ma ho riscontrato un problema nell'usarlo in combinazione con la ripetizione di viste parziali, ho pubblicato la domanda qui: stackoverflow.com/questions/28372330/…
Josh

Un bell'esempio di prendere la soluzione semplice e renderla molto elegante, nello spirito di MVC. Molto bella!
AHowgego

7

Perché non creare una funzione privata con la logica nel metodo "Create" e chiamare questo metodo sia dal metodo Get che da quello Post e restituire semplicemente View ().


Questo è effettivamente quello che ho finito per fare - mi hai letto nel pensiero. +1 :)
RPM1984

1
Questo è quello che faccio anche io, solo che invece di avere una funzione privata, ho semplicemente il mio metodo POST che chiama il metodo GET in caso di errore (cioè la return Create(new { uniqueUri = ... });tua logica rimane ASCIUTTA (molto simile alla chiamata RedirectToAction), ma senza i problemi portati dal reindirizzamento, come perdere il tuo ModelState
Daniel Liuzzi

1
@DanielLiuzzi: così facendo non cambierà l'URL. Quindi finisci con url qualcosa come "/ controller / create /".
Skorunka František

@ SkorunkaFrantišek E questo è esattamente il punto. La domanda afferma Se si verifica un errore, è necessario visualizzare la stessa pagina con l'errore. In questo contesto, è perfettamente accettabile (e preferibile IMO) che l'URL NON cambi se viene visualizzata la stessa pagina. Inoltre, un vantaggio di questo approccio è che se l'errore in questione non è un errore di convalida ma un errore di sistema (ad esempio timeout DB), consente all'utente di aggiornare semplicemente la pagina per inviare nuovamente il modulo.
Daniel Liuzzi

4

Potrei usare TempData["Errors"]

TempData vengono passati attraverso azioni preservando i dati 1 volta.


4

Ti suggerisco di restituire la visualizzazione ed evitare la duplicazione tramite un attributo sull'azione. Ecco un esempio di popolamento per visualizzare i dati. Potresti fare qualcosa di simile con la logica del tuo metodo di creazione.

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
}

Ecco un esempio:

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

Come è una cattiva idea? Penso che l'attributo eviti la necessità di utilizzare un'altra azione perché entrambe le azioni possono utilizzare l'attributo per caricare in ViewData.
CRice

1
Dai un'occhiata a Post / Redirect / Get pattern: en.wikipedia.org/wiki/Post/Redirect/Get
DreamSonic

2
Viene normalmente utilizzato dopo che la convalida del modello è stata soddisfatta, per impedire ulteriori post nello stesso modulo all'aggiornamento. Ma se il modulo presenta problemi, deve essere corretto e ripubblicato comunque. Questa domanda riguarda la gestione degli errori del modello.
CRice

I filtri sono per il codice riutilizzabile sulle azioni, particolarmente utili per mettere le cose in ViewData. TempData è solo una soluzione alternativa.
CRice

1
@ppumkin forse prova a postare con ajax in modo da non avere difficoltà a ricostruire la tua vista lato server.
CRice

2

Ho un metodo che aggiunge lo stato del modello ai dati temporanei. Quindi ho un metodo nel mio controller di base che controlla i dati temporanei per eventuali errori. Se li ha, li aggiunge di nuovo a ModelState.


1

Il mio scenario è un po 'più complicato poiché sto usando il pattern PRG, quindi il mio ViewModel ("SummaryVM") è in TempData e la mia schermata di riepilogo lo visualizza. C'è un piccolo modulo in questa pagina per POSTARE alcune informazioni a un'altra azione. La complicazione è derivata dalla richiesta per l'utente di modificare alcuni campi in SummaryVM in questa pagina.

Summary.cshtml contiene il riepilogo della convalida che rileverà gli errori ModelState che creeremo.

@Html.ValidationSummary()

Il mio modulo ora deve essere POST a un'azione HttpPost per Summary (). Ho un altro ViewModel molto piccolo per rappresentare i campi modificati e il modelbinding me li procurerà.

La nuova forma:

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

e l'azione ...

[HttpPost]
public ActionResult Summary(EditedItemsVM vm)

Qui faccio una convalida e rilevo qualche input errato, quindi devo tornare alla pagina Riepilogo con gli errori. Per questo utilizzo TempData, che sopravviverà a un reindirizzamento. Se non ci sono problemi con i dati, sostituisco l'oggetto SummaryVM con una copia (ma con i campi modificati ovviamente cambiati) poi eseguo 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'azione del controller di riepilogo, dove inizia tutto questo, cerca eventuali errori nei dati temp e li aggiunge allo stato del modello.

[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

Microsoft ha rimosso la possibilità di memorizzare tipi di dati complessi in TempData, quindi le risposte precedenti non funzionano più; puoi memorizzare solo tipi semplici come le stringhe. Ho modificato la risposta di @ asgeo1 affinché funzioni come previsto.

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

Da qui, puoi semplicemente aggiungere l'annotazione dei dati richiesta su un metodo del controller secondo necessità.

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


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

Funziona perfettamente !. Modificata la risposta per correggere un piccolo errore di parentesi quando si incolla il codice.
VDWWD

0

Preferisco aggiungere un metodo al mio ViewModel che popola i valori predefiniti:

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

Quindi lo chiamo quando mai ho bisogno dei dati originali in questo modo:

    [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

Sto dando solo un codice di esempio qui Nel tuo viewModel puoi aggiungere una proprietà di tipo "ModelStateDictionary" come

public ModelStateDictionary ModelStateErrors { get; set; }

e nel tuo metodo di azione POST puoi scrivere codice direttamente come

model.ModelStateErrors = ModelState; 

e quindi assegna questo modello a Tempdata come di seguito

TempData["Model"] = model;

e quando si reindirizza al metodo di azione di un altro controller, nel controller è necessario leggere il valore Tempdata

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

Questo è tutto. Non è necessario scrivere filtri di azione per questo. Questo è semplice come il codice precedente se si desidera ottenere errori di stato del modello in un'altra vista di un altro controller.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.