Posts tagged MVC2

A Poll: Web Api Calls

A couple of days ago I threw out a poll on twitter to see what people thought of a particular question I had in mind. Here it is:

What I’m after is pretty simple … how should one handle exceptional cases in Web Api development. Namily JSON and XML. My take on it is pretty simple. If I have an API call that is a HTTP GET for the following URL: http://example.org/api/v1/users/get_orders.json

I feel I should get a list of orders back.

What if there are no orders to return (maybe this is a new account, maybe the person who owns this account deleted them all, etc)? I feel I should get back an empty list of orders. However, some API’s on the web (that shall remain nameless at this time) return this scenario as a “error document”. The problem with this is that an “error document” should be used for errors and exceptions, not known and valid use cases.

Example: if I’m required to pass in my API KEY through the HTTP Headers and I give your API an invalid key, then I would expect that you return an error document back to the caller (my client app). That is an error! An empty list of orders is a valid use case.

Let my client app decide what I should do, I’m consuming your data, you’re not consuming my application.

What’s your opinion? (Please vote as well, thanks!)

ASPNET MVC: Handling Multiple Buttons on a Form with jQuery

Sometimes your task in MVC involves many buttons in the same form. Such as the screen shot here.

What happens in most situations is that you end up having some code that kind of looks like this:


<form action="/foo/save">

<!-- My HTML and multiple buttons -->

</form>

… and then the code looks like this


public ActionResult Save(CustomerList customers, string buttonName)

{

if(buttonName == "Save")

{

// do something

}

else if (buttonName == "Reload")

{

// etc, etc

}
// Other if's, etc, etc

}

We can solve this with a little jQuery. By attaching a click handler to the button we can tell the form where we want it to go. Therefore keeping our controllers actions very slim.

Here’s the jQuery:


// This assumes your buttons have id's "save-button" and "update-button"

$("#save-button").click(function () {

 // Redirect to add facility for Credit Exposure
 var form = $(this).parents('form:first');
 form.attr('action', '<%=Url.RouteUrl(new { controller = "foo", action = "save"  }) %>');
});

$("#reload-button").click(function () {

 // Redirect to add facility for Credit Exposure
 var form = $(this).parents('form:first');
 form.attr('action', '<%=Url.RouteUrl(new { controller = "foo", action = "reload"  }) %>');
});

Now when the user clicks “Save” they will go to the “Save” Action on the “FooController”. When the user clicks “Reload” they will go to the “Reload” action on the”FooController”. No more logic switches in the Controller.

You can download and example here (minus the MVC Goo):

MVC: Unit Testing Action Filters

Certain parts of ASP.NET MVC can be a real pain to test. Namely Model Binders, Action Filters and anything that relies on some magic “Context” that seems to derive from HttpContext, ControllerContext, RequestContext, etc.

Below, I’ve outlined how I’ve unit tested a Action Filter. You could extrapolate some of this code into a base Test Class, but I’ve left it all in one place so you can see whats going on.

Background

The action filter filters out Employees based upon a “group”. Employee id’s are in a table and if the employee id has a parent id it is part of a group. I want to be able to put this action filter on any action or controller where groups should not be allowed. If the employee is part of a group, I want to change the ViewResult to ContentResult and stuff some html in there for the user to see. Therefore if the user cannot cannot access this screen, they’ll still see the master page, etc, but the view will be some plain text letting them know that they cannot view the page through a group.

Here’s the code:

using System.Web.Mvc;
using SharpArch.Core;

namespace Agilevent.UI.Web.ActionFilters
{
    public class EmployeeGroupRestrictedActionFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            // Get Employee Id
            var employeeId = int.Parse(filterContext.RouteData.GetRequiredString("employeeId"));
            var employeeInfo = SafeServiceLocator<IEmployeeInfoService>.GetService().GetById(employeeId);

            if (employeeInfo.IsGroup)
            {
                filterContext.Result = new ContentResult {Content = HtmlResultString};
            }

            base.OnActionExecuted(filterContext);
        }

        public string HtmlResultString
        {
            get { return "This screen is only accessible by selecting a the actual employee. You cannot view it through a group id."; }
        }

    }
}

The Unit Test

The unit test mocks out the Action Executed Context with the values that I need mocked out. I make sure that the RouteData contains a value that I need and then the Service Locator is set up to return a mock service which will return a mocked out Employee with a  known state. Since the employee is a group the test will fall into the ContentResult and we can test from there.

Here’s the code:

using System;
using System.Security.Policy;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.Practices.ServiceLocation;
using Moq;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
using StructureMap;
using StructureMap.ServiceLocatorAdapter;

namespace Agilevent.UnitTests.MVC.ActionFilters
{
    [TestFixture]
    public class EmployeeGroupRestrictedActionFilterTests
    {
        private Container _container;
        private Mock<IEmployeeInfoService> _employeeInfoService;
        private EmployeeInfo _employeeInfo;

        [Test]
        public void should_not_be_able_to_continue_if_the_employee_id_is_a_group()
        {
            // Set up fake employee info as a group
            _employeeInfo = new EmployeeInfo();
            _employeeInfo.IsGroup = true;

            // Set up employee info service to return the fake employee when called with "123"
            _employeeInfoService = new Mock<IEmployeeInfoService>();
            _employeeInfoService.Setup(b => b.GetById(It.Is<int>(i => i == 123))).Returns(_employeeInfo);

            // Set up the container & Service Locator
            _container = new Container();
            _container.Configure(x => x.For<IEmployeeInfoService>().Use(_employeeInfoService.Object));
            ServiceLocator.SetLocatorProvider(() => new StructureMapServiceLocator(_container));

            // Mock out the context to run the action filter.
            var request = new Mock<HttpRequestBase>();
            var httpContext = new Mock<HttpContextBase>();
            httpContext.SetupGet(c => c.Request).Returns(request.Object);

            var routeData = new RouteData(); //
            routeData.Values.Add("employeeId", "123");

            var actionExecutedContext = new Mock<ActionExecutedContext>();
            actionExecutedContext.SetupGet(r => r.RouteData).Returns(routeData);
            actionExecutedContext.SetupGet(c => c.HttpContext).Returns(httpContext.Object);

            var filter = new EmployeeGroupRestrictedActionFilterAttribute();

            filter.OnActionExecuted(actionExecutedContext.Object);

            // Assert
            Assert.That(actionExecutedContext.Object.Result, Is.InstanceOfType(typeof(ContentResult)));
            Assert.That((actionExecutedContext.Object.Result as ContentResult).Content, Is.EqualTo(filter.HtmlResultString));
        }

    }
}

I hope that helps someone out there, I know I’ll probably even come back at some point in the future to see how I did it. :) Enjoy.

jQuery Tip: Brute Force Dirty Form Detection

If you disable your “save” button on your MVC views when the page first loads and want them to be auto-enabled when any input on the form changes, you can do that with the following jQuery snippet.

This is sort of a hack*, but it works.

With this HTML


<input type="submit" id="myButton" value=" Save " disabled />

and this javascript:


<script>
 $(function () {
$(":input").change(function () {
// if any input element changes, enable the save and undo buttons
$("#<%=myButton.ClientID %>").removeAttr('disabled');
});
 });
</script>

You’ll “enable” the button when the form inputs are changed. This is kind of nice when you have a TON of inputs on the page and you need to be able to detect when ANY of them have changed.

* Why its a Hack: I’m not limiting the selector to an ‘id’ therefore sizzle scans the entire DOM which could be a bummer in regards to performance).

No IE6 ActionFilter for ASP.NET MVC

*** Update ***: I’ve also created a cheap jQuery plugin for this as well. See the bottom of the post

There’s a lot of talk on Twitter today about IE6. I’m building a web application in my free time and I didn’t want to support IE6. Will I lose users? Yes, a few, but I don’t care. To try to help circumvent someone with IE6 hitting my site I wrote an ASP.NET MVC Action Filter to block anything less than IE7. It’s rather crude, but it works.

Please note, I’ve only tested this with IE Tester, so real IE6 users… well YMMV.

Here’s the code:


public class NoIe6Attribute : ActionFilterAttribute
 {

 public override void OnActionExecuting(ActionExecutingContext filterContext)
 {
 var browser = filterContext.HttpContext.Request.Browser.Browser;
 var major = filterContext.HttpContext.Request.Browser.MajorVersion;

 if(browser.ToLowerInvariant() == "ie" && major <= 6)
 {
 filterContext.Result = new ContentResult {Content = Ie6NotSupportedHtml};
 }

 }

 public string Ie6NotSupportedHtml
 {
 get
 {
 return @"<html>
<head>
 <title>IE6 Not Supported</title>
 <style>

 body {
 background-color: #111;
 color: #FFF;
 font-family: Trebuchet MS, 'Helvetica Neue', Arial, sans-serif;
 font-weight: light;
 letter-spacing: 1px;
 line-height: 1.5;
 }

 h1 {
 font-size: 60px;
 font-weight: bold;
 line-height: 1;
 margin: 40px 10px 10px;
 text-align: center;
 }

 h2 {
 font-size: 20px;
 font-weight: normal;
 line-height: 1.2;
 margin-bottom: 20px;
 margin-right: 10px;
 margin-left: 10px;
 text-align: center;
 }

 a:link,
 a:visited,
 a:hover,
 a:focus,
 a:active {
 border: none;
 color: #5EA9FF;
 font-weight: bold;
 letter-spacing: 1.5px;
 text-decoration: none;
 }

 .container {
 margin: 0 auto;
 width: 540px;
 }

 </style>
 </head>
 <body>
 <divcontainer>
 <h1>Sorry, we do not support IE6.</h1>
 <h2>Please <a href=""http://www.microsoft.com/ie"" target=""_blank"" title=""Upgrade IE"">upgrade your browser</a> to a recent version of Internet Explorer</h2></div>
 </body>
</html>";
 }
 }

 }

Explaining It

It’s crude, but so it IE6, so we’re fighting fire with fire here. You don’t need to include any fancy HTML or whatever. If this filter finds that you’re running IE6, it will change the ViewResult to a ContentResult and stuff some hard-coded HTML into that result.

Result

This is what you’ll see if you visit an action/controller/etc that is decorated with this attribute. (click for bigger image)

How to Use It

Personally I like to block _all_ IE6 access (for my current project), so I throw it on my BaseController (the class all of my controllers inherit from). You can also slap it on an action, or you can slap it on a single controller if you’re doing some wonky stuff in an individual controller.


[NoIe6]
public class HomeController : Controller
{
 public ActionResult Index()
 {
 ViewData["Message"] = "Welcome to ASP.NET MVC!";

 return View();
 }

}

// or on an action

public class HomeController : Controller
{
 [NoIe6]
 public ActionResult Index()
 {
 ViewData["Message"] = "Welcome to ASP.NET MVC!";

 return View();
 }

}

Enjoy.

Let me know if you see any errors with it. I whipped it up a few weeks ago and never touched it since.

Update

jQuery Plug-In

Some people said they would rather have this as a jQuery plug-in. I understand that, but I still prefer to go on the server side. That way I don’t get the entire HTML of the page sent back to the user. They get what I give them when they use IE6. However, not everyone thinks like me. So here is a jQuery plugin for it.

Download the Plug-in: jquery.noie6.js

Code:


/**
 * jQuery noie6
 * A jQuery plugin to display text for IE6 Users.
 *
 * v0.0.1 - 31 March 2010
 *
 * Copyright (c) 2010 Donn Felker (http://twitter.com/donnfelker)
 * Dual licensed under the MIT and GPL licenses.
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.opensource.org/licenses/gpl-license.php
 *
 * Use $.noie6();
 *
 **/
;jQuery.noie6 = function (x) {
 // Stolen from thickbox
 if (typeof document.body.style.maxHeight === "undefined") {
 $("body").html("<style>        body {background-color: #111;color: #FFF;font-family: Trebuchet MS, 'Helvetica Neue', Arial, sans-serif;            font-weight: light;            letter-spacing: 1px;            line-height: 1.5;        }        h1 {    color:#FFF;        font-size: 60px;            font-weight: bold;            line-height: 1;            margin: 40px 10px 10px;            text-align: center;        }        h2 {    color: #FFF;        font-size: 20px;            font-weight: normal;            line-height: 1.2;            margin-bottom: 20px;            margin-right: 10px;            margin-left: 10px;            text-align: center;        }        a:link,        a:visited,        a:hover,        a:focus,        a:active {            border: none;            color: #5EA9FF;            font-weight: bold;            letter-spacing: 1.5px;            text-decoration: none;        }        .container {            margin: 0 auto;            width: 540px;        }    </style><div class=\"container\">        <h1>Sorry, we do not support IE6.</h1>        <h2>Please <a href=\"http://www.microsoft.com/ie\" target=\"_blank\" title=\"Upgrade IE\">upgrade your browser</a> to a recent version of Internet Explorer</h2>    </div>");
 }
};

Usage:


<script>

$(function() { $.noie6(); }

</script>

This will give you the same screen I showed above (black background, white text, etc). However, this will not get run until document.ready _and_ you will have a whole goop of HTML that came down with the page. Note: The browser detection is from some Thickbox code.