75

How can I return the result of a different action or move the user to a different action if there is an error in my ModelState without losing my ModelState information?

The scenario is; Delete action accepts a POST from a DELETE form rendered by my Index Action/View. If there is an error in the Delete I want to move the user back to the Index Action/View and show the errors that are stored by the Delete action in the ViewData.ModelState. How can this be done in ASP.NET MVC?

[AcceptVerbs(HttpVerbs.Post | HttpVerbs.Delete)]
public ActionResult Delete([ModelBinder(typeof(RdfUriBinder))] RdfUri graphUri)
{
    if (!ModelState.IsValid)
        return Index(); //this needs to be replaced with something that works :)

    return RedirectToAction("Index");
}
Amal K
  • 4,359
  • 2
  • 22
  • 44
Eric Schoonover
  • 47,184
  • 49
  • 157
  • 202

6 Answers6

103

Store your view data in TempData and retrieve it from there in your Index action, if it exists.

   ...
   if (!ModelState.IsValid)
       TempData["ViewData"] = ViewData;

   RedirectToAction( "Index" );
}

 public ActionResult Index()
 {
     if (TempData["ViewData"] != null)
     {
         ViewData = (ViewDataDictionary)TempData["ViewData"];
     }

     ...
 }

[EDIT] I checked the on-line source for MVC and it appears that the ViewData in the Controller is settable, so it is probably easiest just to transfer all of the ViewData, including the ModelState, to the Index action.

Amal K
  • 4,359
  • 2
  • 22
  • 44
tvanfosson
  • 524,688
  • 99
  • 697
  • 795
  • ViewData.ModelState does not have a setter. – Eric Schoonover Nov 11 '08 at 01:34
  • ok. copy instead of set. didn't have access to VS to check it from home. – tvanfosson Nov 11 '08 at 01:43
  • you'll need to check the syntax of the copy function -- again, no VS at home. – tvanfosson Nov 11 '08 at 01:45
  • 2
    That works... Thanks! Wish there was a cleaner way to do this... maybe this is clean, but it seems like a common task that should be part of a RedirectToAction override or something. – Eric Schoonover Nov 11 '08 at 05:20
  • 6
    You could refactor it away to actionfilters, as suggested here (see bullet 13): http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx – Tomas Aschan Apr 10 '10 at 20:20
  • 5
    Something to note: TempData uses Session by default. And if your session is backed by the StateServer or SqlServer, then this technique will run into problems when it tries to Serialize the ViewData, since the ViewDataDictionary type is not marked as Serializable. – 9b5b Nov 04 '11 at 21:17
  • 1
    Indeed, since ViewDataDictionary is not Serializable, it is not possible to persist ViewData between requests in TempData if the "Session" is anything other than "In-Process" which makes this solution incompatible with web farms, session state service, cookie temp data provider, etc :( – Novox Jun 24 '14 at 19:15
  • Then what can we do to maintain `ModelState` in `SqlServer` session mode? – Blaise Feb 07 '17 at 15:37
  • @Blaise what I'd probably do in today's world is handle the data deletion via AJAX and not move the user until after the deletion was confirmed, handling that part client-side based on the data delivered as part of the AJAX (API) request. – tvanfosson Feb 07 '17 at 19:58
  • Works perfectly for me. I'm also setting the model in `TempData["USER"] = user;` and then if `TempData["USER"]` is not null I'm returning it in the View `return View((User)TempData["USER"]); ` – neildt Jan 17 '20 at 17:53
42

Use Action Filters (PRG pattern) (as easy as using attributes)

Mentioned here and here.

James Hill
  • 60,353
  • 20
  • 145
  • 161
bob
  • 6,465
  • 1
  • 30
  • 26
  • 3
    Best answer for this problem IMO. – Jean-Francois May 26 '10 at 13:04
  • 2
    Yes this is the answer. Not sure why those action attributes aren't in the MVC framework though, as it's quite a common scenario – Chris S Mar 30 '11 at 18:07
  • ModelState.Merge() was what I was looking for. Great link as well. +1 – ryanulit Sep 05 '12 at 17:44
  • Link to Rashid's blog is gone from ASP.NET. That seems odd. However, his solution and the relevant code is also discussed [here](http://benfoster.io/blog/automatic-modelstate-validation-in-aspnet-mvc). – Marc L. Aug 03 '16 at 19:03
12

Please note that tvanfosson's solution will not always work, though in most cases it should be just fine.

The problem with that particular solution is that if you already have any ViewData or ModelState you end up overwriting it all with the previous request's state. For example, the new request might have some model state errors related to invalid parameters being passed to the action, but those would end up being hidden because they are overwritten.

Another situation where it might not work as expected is if you had an Action Filter that initialized some ViewData or ModelState errors. Again, they would be overwritten by that code.

We're looking at some solutions for ASP.NET MVC that would allow you to more easily merge the state from the two requests, so stay tuned for that.

Thanks, Eilon

Eilon
  • 25,582
  • 3
  • 84
  • 102
  • Hey Eilon, Is @bob's answer (from Kazi Manzur Rashid's blog) still the best way to do this, or do the MVC team recommend some other method currently? – Patrick McDonald Apr 25 '14 at 16:48
  • 1
    @PatrickMcDonald There's nothing new in MVC that I can think of that would solve this. However, I would caution against completely overwriting all of ViewData and instead being more selective about what gets copied over from the previous request to the new request. – Eilon Apr 25 '14 at 20:05
  • ModelState has a `Merge` function. – Chris Haines May 18 '17 at 16:00
5

In case this is useful to anyone I used @bob 's recommended solution using PRG:

see item 13 -> link.

I had the additional issue of messages being passed in the VeiwBag to the View being written and checked / loaded manually from TempData in the controller actions when doing a RedirectToAction("Action"). In an attempt to simplify (and also make it maintainable) I slightly extended this approach to check and store/load other data as well. My action methods looked something like:

 [AcceptVerbs(HttpVerbs.Post)]
 [ExportModelStateToTempData]
 public ActionResult ChangePassword(ProfileViewModel pVM) {
      bool result = MyChangePasswordCode(pVM.ChangePasswordViewModel);
      if (result) {
           ViewBag.Message = "Password change success";
      else {
           ModelState.AddModelError("ChangePassword", "Some password error");
      }
      return RedirectToAction("Index");
    }

And my Index Action:

[ImportModelStateFromTempData]
public ActionResult Index() {
    ProfileViewModel pVM = new ProfileViewModel { //setup }
    return View(pVM);
}

The code in the Action Filters:

// Following best practices as listed here for storing / restoring model data:
// http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx#prg
public abstract class ModelStateTempDataTransfer : ActionFilterAttribute {
    protected static readonly string Key = typeof(ModelStateTempDataTransfer).FullName;
}

:

public class ExportModelStateToTempData : ModelStateTempDataTransfer {
    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        //Only export when ModelState is not valid
        if (!filterContext.Controller.ViewData.ModelState.IsValid) {
            //Export if we are redirecting
            if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult)) {
                filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState;
            }
        }
        // Added to pull message from ViewBag
        if (!string.IsNullOrEmpty(filterContext.Controller.ViewBag.Message)) {
            filterContext.Controller.TempData["Message"] = filterContext.Controller.ViewBag.Message;
        }

        base.OnActionExecuted(filterContext);
    }
}

:

public class ImportModelStateFromTempData : ModelStateTempDataTransfer {
    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;

        if (modelState != null) {
            //Only Import if we are viewing
            if (filterContext.Result is ViewResult) {
                filterContext.Controller.ViewData.ModelState.Merge(modelState);
            } else {
                //Otherwise remove it.
                filterContext.Controller.TempData.Remove(Key);
            }
        }
        // Restore Viewbag message
        if (!string.IsNullOrEmpty((string)filterContext.Controller.TempData["Message"])) {
            filterContext.Controller.ViewBag.Message = filterContext.Controller.TempData["Message"];
        }

        base.OnActionExecuted(filterContext);
    }
}

I realize my changes here are a pretty obvious extension of what was already being done with the ModelState by the code @ the link provided by @bob - but I had to stumble on this thread before I even thought of handling it in this way.

Matthew
  • 9,851
  • 4
  • 46
  • 77
  • Thank you, just need to edit line of ImportModelStateFromTempData for partial views >> if (filterContext.Result is ViewResult || filterContext.Result is PartialViewResult) – k4st0r42 Feb 18 '15 at 08:00
0

Please don't skewer me for this answer. It is a legitimate suggestion.

Use AJAX

The code for managing ModelState is complicated and (probably?) indicative of other problems in your code.

You can pretty easily roll your own AJAX javascript code. Here is a script I use:

https://gist.github.com/jesslilly/5f646ef29367ad2b0228e1fa76d6bdcc#file-ajaxform

(function ($) {

    $(function () {

        // For forms marked with data-ajax="#container",
        // on submit,
        // post the form data via AJAX
        // and if #container is specified, replace the #container with the response.
        var postAjaxForm = function (event) {

            event.preventDefault(); // Prevent the actual submit of the form.

            var $this = $(this);
            var containerId = $this.attr("data-ajax");
            var $container = $(containerId);
            var url = $this.attr('action');

            console.log("Post ajax form to " + url + " and replace html in " + containerId);

            $.ajax({
                type: "POST",
                url: url,
                data: $this.serialize()
            })
                .done(function (result) {
                    if ($container) {
                        $container.html(result);
                        // re-apply this event since it would have been lost by the form getting recreated above.
                        var $newForm = $container.find("[data-ajax]");
                        $newForm.submit(postAjaxForm);
                        $newForm.trigger("data-ajax-done");
                    }
                })
                .fail(function (error) {
                    alert(error);
                });
        };
        $("[data-ajax]").submit(postAjaxForm);
    });

})(jQuery);
Jess
  • 23,901
  • 21
  • 124
  • 145
-3

Maybe try

return View("Index");

instead of

return Index();
Ty.
  • 2,220
  • 1
  • 15
  • 14
  • That doesn't work, as it doesn't execute the logic in the Index Action. All it does is try to render the current model using the Index view. – Eric Schoonover Nov 11 '08 at 00:42
  • Don't you just want to show the model errors on the same view that you posted from? What are you doing in the Index action that needs to be executed when there are model errors? I just return View("ViewName", model) when there are errors and it works fine. – Ty. Nov 11 '08 at 01:43
  • No, I want to redirect to the Index action bind the view to the data generated by that action as well as the ModelState that was defined by the failed Delete action. – Eric Schoonover Nov 11 '08 at 04:45
  • I don't understand the negative votes, because it is actually a good answer and you can take your "logic" from the Index to a service that produces the model for your index view. And after do this: return View("Index", modelObjectThatProducedByYourService). It the ModelState will be maintained to display errors in the Index view. – dvjanm Dec 04 '13 at 15:41