Saturday, April 3, 2010

Breaking? Change In Asp.Net MVC2

In MVC1, by default an empty textbox was sent as string.Empty to the controller on POST. In MVC2, it now sends the value as NULL. See ModelMetadata.ConvertEmptyStringToNull. I think there are some valid reasons for why they made the change – see Brad Wilson’s comment for more. If your system relies on string.Empty, you’ve got a problem.

There are several options for handling this. The first is using the DisplayFormatAttribute on every property that needs it. Painful. Here’s an example:

public string FirstName { get; set; }

You can also write a custom binder that sets this value to false. Like so:

public class CustomStringModelBinder : DefaultModelBinder 
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)

        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);   
        if (bindingContext.ModelType == typeof(string))
            bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
            if (value == null || string.IsNullOrEmpty(value.AttemptedValue))   
                return "";
        return value.AttemptedValue;

You’ll need to add the following to the Global.asax Application_Start method to use your custom model binder.

ModelBinders.Binders.Add(typeof(string), new CustomStringModelBinder());

One thing to note, if you use the RequiredAttribute, you don’t need to worry about the null because it will invalidate the model based on the NULL.

Which option is better? Neither. Simply pick the default for ConvertEmptyStringToNull and then use DisplayFormat to handle the exceptions. Not perfect but not bad.

I do wish that this was configurable in some way instead of creating a ModelBinder.

Thursday, April 1, 2010

Navigation Using Areas in Asp.Net MVC

Asp.Net MVC2 introduced Areas as a feature part of the framework. Areas allow you to divide your application into logical units. It also makes your Url’s look more accurate.

Thinking of a generic store they might have areas such as Inventory, Payroll, Employees and Sales. Here are example ActionLinks for each area.

<%= Html.ActionLink("Home", "Index", "Home", new { area = "" }, new { })%>
<%= Html.ActionLink("Payroll", "Index", "Main", new { area = "Payroll" }, new { })%>
<%= Html.ActionLink("Inventory", "Index", "Main", new { area = "Inventory" }, new { })%>
<%= Html.ActionLink("Employees", "Index", "Main", new { area = "Employees" }, new { })%>
<%= Html.ActionLink("Sales", "Index", "Main", new { area = "Sales" }, new { })%>

Notice that the area is passed as a route value. I wrote my own overload for the ActionLink that takes the Area as an argument.

public static MvcHtmlString ActionLink(this HtmlHelper helper, string linkText, string actionName, string controllerName, string areaName, object routeValues, object htmlAttributes)
    RouteValueDictionary routes = new RouteValueDictionary(routeValues);
    RouteValueDictionary attributes = new RouteValueDictionary(htmlAttributes);
    routes.Add("area", areaName);
    return helper.ActionLink(linkText, actionName, controllerName, routes, attributes);

This makes our links look as follows:

<%= Html.ActionLink("Home", "Index", "Home", "", new { }, new { })%>
<%= Html.ActionLink("Payroll", "Index", "Main", "Payroll", new { }, new { })%>
<%= Html.ActionLink("Inventory", "Index", "Main", "Inventory", new { }, new { })%>
<%= Html.ActionLink("Employees", "Index", "Main", "Employees", new { }, new { })%>
<%= Html.ActionLink("Sales", "Index", "Main", "Sales", new { }, new { })%>

If you haven’t worked with Areas yet, your probably looking at the routes and noticing that each area link has a controller called Main with an Index method. You might think we’re calling the same controller and passing the area name as parameter. That is not what we are doing, each area has a controller named Main. Maybe the generated links will help.

Next thing you should notice is that the link for returning to the Home page has an empty area. We need this so that when we’re in an area we can move back up to the root. Otherwise, we’ll get a Resource Not Found error indicating showing that it’s looking for Home in that Area. This can also work in your favor. If you have a Home controller (or Main) in all of your areas, MVC will, by default, navigate within the Area and you don’t need to change your Site.Master.

Overall, areas are a great feature for keeping your applications organized.