Anybody that's been on the internet for more than five seconds has encountered one of these:

I'm a fan of getting rid of anything that interferes with the user experience, and these dialogs certainly get in the way. There's a pattern we can implement, called POST-REDIRECT-GET, that will eliminate these dialogs. Let's see what that pattern is, and how we can implement it in a simple ASP.NET MVC application.

What is PRG?

POST-REDIRECT-GET is a pattern that says a POST action should always REDIRECT to a GET action. This pattern is meant to provide a more intuitive interface for users, specifically by reducing the number of duplicate form submissions.

The Normal Way

Here's the code files we'll use for a regular POST scenario:

ViewModels/Home/AddUserVM.cs
public class AddUserVM  
{
    [DisplayName("First Name:")]
    [Required(ErrorMessage = "Please enter a first name.")]
    public string FirstName { get; set; }

    [DisplayName("Last Name:")]
    [Required(ErrorMessage = "Please enter a last name.")]
    public string LastName { get; set; }

    [DisplayName("Date of Birth:")]
    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-mm-dd}", ApplyFormatInEditMode = true)]
    [Required(ErrorMessage = "Please select a date of birth.")]
    public DateTime DateOfBirth { get; set; }
}
Controllers/HomeController.cs
public class HomeController : Controller  
{
    [HttpGet]
    public ActionResult Normal()
    {
        AddUserVM model = new AddUserVM();
        return View(model);
    }

    [HttpPost]
    public ActionResult Normal(AddUserVM model)
    {
        if(!ModelState.IsValid)
        {
            return View(model);
        }

        return RedirectToAction("Index");
    }
}
Views/Home/Normal.cshtml
@model StrictPRGDemo.ViewModels.Home.AddUserVM

<h2>Add a User (Normal)</h2>

@using(Html.BeginForm())
{
    <div>
        <div>
            @Html.LabelFor(x => x.FirstName)
            @Html.TextBoxFor(x => x.FirstName)
            @Html.ValidationMessageFor(x => x.FirstName)
        </div>
        <div>
            @Html.LabelFor(x => x.LastName)
            @Html.TextBoxFor(x => x.LastName)
            @Html.ValidationMessageFor(x => x.LastName)
        </div>
        <div>
            @Html.LabelFor(x => x.DateOfBirth)
            @Html.EditorFor(x => x.DateOfBirth)
            @Html.ValidationMessageFor(x => x.DateOfBirth)
        </div>
        <div>
            <input type="submit" value="Save" />
        </div>
    </div>
}

Our rendered page looks like this:

If we immediately click Save, our validation fires:

But what happens if we refresh the page? We get the validation warning:

Let's get rid of that, using PRG.

The PRG Way

PRG says that all POSTS need to redirect to a GET action, which sounds easy enough. But if we try this in a naive solution, where instead of returning the View(model) when validation fails we just redirect back to the GET action, the validation messages and input values will not appear.

Here's the problem: those validation messages and input values are stored in the ModelState, which gets recreated when moving between actions. We need a way to save the model state somewhere that we can access it later.

Lucky for us, there's a data structure called TempData. TempData allows data to exist for the current request and the next one, and then the data gets deleted. Sounds like that'll fill our needs, don't it?

In fact, six years ago, Kazi Mansur Rashid wrote a blog post that laid out exactly how we can use TempData to store the ModelState, and even better, how we can wrap that action in a set of attributes.

Here's those attributes:

Attributes/ModelStateTransfer.cs
public abstract class ModelStateTransfer : ActionFilterAttribute  
{
    protected static readonly string Key = typeof(ModelStateTransfer).FullName;
}

public class ExportModelStateAttribute : ModelStateTransfer  
{
    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;
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

public class ImportModelStateAttribute : ModelStateTransfer  
{
    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);
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

This creates two attributes: ExportModelState and ImportModelState.

ExportModelState takes the current ModelState and stores it in TempData, and ImportModelState reads from TempData and merges the found ModelState (if it exists) into the new one. Simple, right?

In fact, in usage, it's even simpler:

Controllers/HomeController.cs
[HttpGet]
[ImportModelState]
public ActionResult Strict()  
{
    AddUserVM model = new AddUserVM();
    return View(model);
}

[HttpPost]
[ExportModelState]
public ActionResult Strict(AddUserVM model)  
{
    if (!ModelState.IsValid)
    {
        return RedirectToAction("Strict");
    }
    return RedirectToAction("Index");
}

Know what's even better? Our ViewModel and View don't have to change at all!

Now, let's imagine we're back at this point:

If we refreshed the page, in the normal scenario we would have gotten the duplicate submission warning, but in our PRG scenario, we get this:

Look at that! We don't get an error, and the page actually refreshed itself rather than trying to resubmit the values. Best of all, the user's experience is that the page did exactly what the user told it to do. It's a little more work than the normal way, but IMO it makes it just that much nicer for our users.

It's just that easy! Check out the sample project on GitHub and let me know what you think of this technique in the comments.