T4MVC model unbinders

Tags: .net, asp.net mvc, t4mvc, model unbinder, model binder

I love T4MVC. It allows me to stop using those ugly magic strings while generating compile-time valid links or accessing resources from ASP.NET MVC views. It also has a very nice feature called IModelUnbinder. In short: it can translate action's argument of any type into properly constructed link.

Arrays (or lists) in action's arguments

Say you have an ASP.NET MVC Controller that has action which accepts long[] ids as argument (the case with IList<long> or any other custom object is the same) and you want to Html.ActionLink(...) it from one of your views so you could for example export some data to PDF or DOCX/XLSX. With T4MVC your call from view will look like this:

@{
    // ids that you want to display
    long[] ids = new long[] { 1, 2, 3, 4 };
}
@Html.ActionLink( "Go to items", MVC.Home.DisplayItemsArray(ids) )

But instead of getting a valid link, you'll end up with something like this: http://localhost:62454/Home/DisplayItemsArray?ids=System.Int64%5B%5D

What happened here? Well, the T4MVC doesn't know how to translate long[] into proper action argument when generating a link. That's when you need to use IModelUnbinder.

IModelUnbinder for long[]

In its simplest form, the IModelUnbinder for long[] will look something like this:

public class LongArrayUnbinder : IModelUnbinder
{
    public void UnbindModel( System.Web.Routing.RouteValueDictionary routeValueDictionary, string routeName, long[] routeValue )
    {
        if( routeValue != null && routeValue.Length > 0 )
        {
            string stringVal = string.Join( ",", routeValue.Select( x => x.ToString() ) );
            routeValueDictionary[ routeName ] = MvcHtmlString.Create( stringVal );
        }
    }
}

It just converts array into String and sets this value for given route value. You can write it once and register it at the application startup. I prefer to put it in ASP.NET MVC's standard RouteConfig class:

public class RouteConfig
{
    public static void RegisterRoutes( RouteCollection routes )
    {
        routes.IgnoreRoute( "{resource}.axd/{*pathInfo}" );

        routes.MapMvcAttributeRoutes();

        // routes goes here...
        // ...

        // register model unbinder
        ModelUnbinderHelpers.ModelUnbinders.Add<long[]>( new LongArrayUnbinder() );
    }
}

And that's it, from now on, all of your Html.ActionLinks will have properly rendered long[] arguments: http://localhost:62454/Home/DisplayItemsArray?ids=1%2C2%2C3%2C4

But as you can see, there's still one small glitch: those %2C encoded commas. By default, Html.ActionLink encodes URLs, so instead of nice 1,2,3,4 you get something that ugly. For the browser it's perfectly valid URL, but for us, humans, it could be nicer (*).

There's actually one thing you could do to make it good-looking. Instead of Html.ActionLink use Url.Action and Server.UrlDecode:

<a class="btn btn-default" href="@HttpContext.Current.Server.UrlDecode( Url.Action(MVC.Home.DisplayItemsArray(ids)) )">Go to items URL</a>

And now we finally end up with our beautiful link: http://localhost:62454/Home/DisplayItemsArray?ids=1,2,3,4

A word of warning: generally, it's always better to encode your URLs or strings before displaying them for user, especially if it might be a user generated content. But in the case when you are 100% sure that there won't be any threats, I guess it should be safe to decode just the URL.

Side note: if you want to unbind any other custom object you could check T4MVC's build-in PropertiesUnbinder:

ModelUnbinderHelpers.ModelUnbinders.Add<YOUR-TYPE>( new PropertiesUnbinder() );

Hold on, it's not the end yet

Unfortunately, there's one more thing do be done. Right now, when you have your action:

public virtual ActionResult DisplayItemsArray( long[] ids )
{
    return View( ids );
}

You'll get null for ids argument. That's because by default in ASP.NET MVC, the DefaultModelBinder accepts arrays a different form: http://localhost:62454/Home/DisplayItemsArray?ids=1&ids=2&ids=3&ids=4

But since we already have our comma separated values, we need also custom ASP.NET MVC's IModelBinder that will translate back our string to long[]. It could be done in many ways, so here's the one way of doing it:

public class LongArrayModelBinder : DefaultModelBinder
{
    public override object BindModel( ControllerContext controllerContext, ModelBindingContext bindingContext )
    {
        var value = bindingContext.ValueProvider.GetValue( bindingContext.ModelName );
        if( value == null || string.IsNullOrEmpty( value.AttemptedValue ) || bindingContext.ModelType != typeof( long[] ) )
            return null;

        return value
            .AttemptedValue
            .Split( ',' )
            .Select( x => { long res = 0; return new { ok = long.TryParse( x, out res ), val = res }; } )
            .Where( x => x.ok )
            .Select( x => x.val )
            .ToArray();
    }
}

You can set it up directly in action's method signature, but I prefer more global approach, so you could just set the model binder above to bind long[] arguments like so:

ModelBinders.Binders.Add( typeof( long[] ), new LongArrayModelBinder() );

Phew, we're done. From now on, our long[] arguments, will be properly unbinded in view and binded again in action call.

For IList<long> or any other ILists or custom objects you want to pass for action, the drill is very similar.

At the first sight it might look like it's a lot to do just to get very simple and basic results. And I agree with that. But the good news is that you only need to set it up at least once per project.

(*) Or maybe you could probably leave it like it is. Ordinary people don't know the difference between a web browser and a search engine, so why bother? When someone like that asks: "Hey, why those links looks so strange?" you have few answers:

  • Strange? They look pretty normal to me.
  • Well, you know, it's the browser decoding URLs so you won't get JS-injection. It's a security feature.
  • It's so that it'll be harder to hack. It's a security feature.
  • It scales better in cloud. It's a scaling-cloudy feature.

With those answers, you'll get the respect you deserve. You're welcome.

Read also

The ugly truth behind pretty URLs

Attribute routing is quite handy feature of ASP.NET MVC. But it can also be the reason your web app is so slooow.

SQL backup to Azure using Powershell

Everybody knows you should be making backups of everything. Without db backup you'll probably be bald soon.

Comments