Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Progressive enhancement in MVC 3 with the help of custom ActionResults

DZone's Guide to

Progressive enhancement in MVC 3 with the help of custom ActionResults

· Web Dev Zone
Free Resource

Learn how to build modern digital experience apps with Crafter CMS. Download this eBook now. Brought to you in partnership with Crafter Software

Last week I wrote a tutorial on Progressive enhancement with ASP.NET MVC 3 and jQuery. It might be a good idea to skim it if you aren’t already familiar with some of the concepts.

A commenter on the last article posed a great question which sparked some improvements to the original code. Thanks for the suggestion!

Quick Recap

As a quick recap, our goal is to build a Contact Us form that works perfectly on all browsers, but offers an enhanced experience for modern browsers with JavaScript enabled.

JavaScript enabled

SNAGHTML1a3b6b08_thumb4

As you can see when a user visits our site with JavaScript enabled and clicks on the Contact Us link, they are presented with a nice jQuery UI dialog window. They can fill in the form and get a nice confirmation message inside the dialog, and finally close it without ever leaving the page they were on.

JavaScript disabled

SNAGHTML1a48993d_thumb1

A visitor without JavaScript will still get the same functionality, just a slightly lesser experience. Without JavaScript logic attached to our Contact Us link it behaves like a plain old hyperlink, navigating the browser to a new page. Once they fill out the form and press Send Message we redirect them back to the Home page with a confirmation message.

 

The new Controller

Per the suggestion from the commenter, I have abstracted the logic into two custom ActionResults named AjaxableViewResult and AjaxableActionResult. Our new controller is much more concise and readable provided we adhere to some conventions for naming our views.

[HttpGet]
public ActionResult ContactUs()
{
    return new AjaxableViewResult();
}
 
[HttpPost]
public ActionResult ContactUs(ContactUsInput input)
{
    if (!ModelState.IsValid)
        return new AjaxableViewResult(input);
 
    // TODO: A real app would send some sort of email here
 
    TempData["Message"] = string.Format("Thanks for the feedback, {0}! We will contact you shortly.", input.Name);
    return new AjaxableActionResult
                {
                    DefaultResult = () => RedirectToAction("Index"),
                    AjaxResult = () => PartialView("_ThanksForFeedback", input)
                };
}

 

The Code

Below you will find the code that abstracts our progressive enhancement logic. The code is documented and should be relatively straight-forward if you’re familiar with MVC’s concept of ActionResults.

AjaxableActionResult

/// <summary>
/// Executes a specific action result depending on whether the incoming request is an Ajax request or not
/// </summary>
public class AjaxableActionResult : ActionResult
{
    /// <summary>
    /// The result to execute for non-Ajax requests
    /// </summary>
    public Func<ActionResult> DefaultResult { get; set; }
 
    /// <summary>
    /// The result the execute for Ajax requests
    /// </summary>
    public Func<ActionResult> AjaxResult { get; set; }
 
    public override void ExecuteResult(ControllerContext context)
    {
        if(DefaultResult == null)
            throw new ArgumentException("The DefaultResult property must be set");
 
        if (AjaxResult == null)
            throw new ArgumentException("The AjaxResult property must be set");
 
 
        if (context.HttpContext.Request.IsAjaxRequest())
        {
            AjaxResult().ExecuteResult(context);
        }
        else
        {
            DefaultResult().ExecuteResult(context);
        }
    }
}

 

AjaxableViewResult

/// <summary>
/// Inspects the incoming request and returns a PartialViewResult for Ajax requests and a full ViewResult for non-Ajax requests
/// </summary>
public class AjaxableViewResult : ActionResult
{
    /// <summary>
    /// Determines the convention for looking up a PartialView for the incoming request.
    /// The default convention looks for an underscore followed by the action name.
    /// For example: _ContactUs.cshtml or _ContactUs.ascx
    /// </summary>
    public static Func<ControllerContext, string> AjaxViewNameConvention = context => "_" + context.RouteData.GetRequiredString("action");
 
    /// <summary>
    /// The view name for non-Ajax requests
    /// </summary>
    public string NonAjaxViewName { get; set; }
 
    /// <summary>
    /// The view name for Ajax requests
    /// </summary>
    public string AjaxViewName { get; set; }
 
    /// <summary>
    /// The model that is rendered to the view
    /// </summary>
    public object Model { get; set; }
 
    /// <summary>
    /// Creates a new AjaxableViewResult using the default conventions
    /// </summary>
    public AjaxableViewResult() : this(null, null, null)
    {
             
    }
 
    /// <summary>
    /// Creates a new AjaxableViewResult using the default conventions with custom model data
    /// </summary>
    public AjaxableViewResult(object model) : this(null, null, model)
    {
     
    }
 
    /// <summary>
    /// Creates a new AjaxableViewResult with a specific ajax partial view
    /// </summary>
    public AjaxableViewResult(string ajaxViewName) : this(ajaxViewName, null)
    {
             
    }
 
    /// <summary>
    /// Creates a new AjaxableViewResult with a specific ajax partial view and model
    /// </summary>
    public AjaxableViewResult(string ajaxViewName, object model) : this(ajaxViewName, null, model)
    {
             
    }
 
    /// <summary>
    /// Creates a new AjaxableViewResult with a specific ajax view, non-ajax view, and model
    /// </summary>
    public AjaxableViewResult(string ajaxViewName, string defaultViewName, object model)
    {
        NonAjaxViewName = defaultViewName;
        AjaxViewName = ajaxViewName;
        Model = model;
    }
 
 
    public override void ExecuteResult(ControllerContext context)
    {
        context.Controller.ViewData.Model = Model;
 
        if (context.HttpContext.Request.IsAjaxRequest())
        {
            var view = new PartialViewResult
                            {
                                ViewName = GetAjaxViewName(context),
                                ViewData = context.Controller.ViewData
                            };
 
            view.ExecuteResult(context);
        }
        else
        {
            var view = new ViewResult
            {
                ViewName = GetViewName(context),
                ViewData = context.Controller.ViewData
            };
 
            view.ExecuteResult(context);   
        }
 
    }
 
    private string GetViewName(ControllerContext context)
    {
        return !string.IsNullOrEmpty(NonAjaxViewName) ? NonAjaxViewName : context.RouteData.GetRequiredString("action");
    }
 
    private string GetAjaxViewName(ControllerContext context)
    {
        return !string.IsNullOrEmpty(AjaxViewName) ? AjaxViewName : AjaxViewNameConvention(context);
    }
}

 

Overriding the AjaxViewName convention

For completeness I decided to add a simple extensibility point to change the Ajax view lookup convention. In your Global.asax you can use the static AjaxViewNameConvention to define your own partial view convention, the default looks for an underscore before the action name.

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
 
    AjaxableViewResult.AjaxViewNameConvention =
        context => "_" + context.RouteData.GetRequiredString("action");
 
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}

 

Wrap-up and source code

Thanks again to the commenter who sparked these improvements!  The full source code is attached to this post, hopefully it helps someone!

download the source

Crafter is a modern CMS platform for building modern websites and content-rich digital experiences. Download this eBook now. Brought to you in partnership with Crafter Software.

Topics:

Published at DZone with permission of Matt Hidinger, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}