ASP.NET MVC Tip #47 – Using ResolveUrl in an HTML Helper

Yesterday, I was creating a simple HTML helper for displaying images. Nothing fancy. The code for the helper is contained in Listing 1.

Listing 1 – HelpersImageHelper.cs

using System.Web.Mvc;
using System.Web.Routing;

namespace MvcApplication1.Helpers
{
    public static class ImageHelper
    {
        public static string Image(this HtmlHelper helper, string id, string url, string alternateText)
        {
            return Image(helper, id, url, alternateText, null);
        }

        public static string Image(this HtmlHelper helper, string id, string url, string alternateText, object htmlAttributes)
        {
            // Create tag builder
            var builder = new TagBuilder("img");

            // Create valid id
            builder.GenerateId(id);

            // Add attributes
            builder.MergeAttribute("src", url);
            builder.MergeAttribute("alt", alternateText);
            builder.MergeAttributes(new RouteValueDictionary(htmlAttributes));

            // Render tag
            return builder.ToString(TagRenderMode.SelfClosing);
        }

    }
}

This helper method uses a TagBuilder to render a standard HTML <img> tag. You can supply the image Id, URL, AlternateText, and HTML attributes when calling the Html.Image() helper.

However, I hit one roadblock. I wanted to enable users to pass application relative URLs to the Html.Image() helper. For example, I wanted users to be able to call the Html.Image() helper within an MVC view like this:

<%= Html.Image(“myImage”, “~/Content/Rover.jpg”, “Picture of Rover”) %>

When you use an ASP.NET Web Forms Web Control, you can assign paths to properties that contain the tilde character (~). For example, you can use a path like ~/Content/Rover.jpg. The control replaces the tilde with the application path automatically. So, you get /MyApp/Content/Rover.jpg. Being able to create application relative URLs is very useful because you might need to move your application to a new virtual directory in the future and you want your URLs to continue to work.

The ASP.NET Web Forms framework uses the Control.ResolveUrl() method to do this. Unfortunately, because this is a method of the Control class, this method is not available within an HTML helper.

I was stuck until Levi — one of the super genius developers on the ASP.NET MVC team — explained that I could use the Url.Content() method to do the same thing as the Control.ResolveUrl() method. So, here is how I can rewrite the Html.Image() helper to support application relative URLs:

Listing 2 – HelpersImageHelper.cs

using System.Web.Mvc;
using System.Web.Routing;

namespace MvcApplication1.Helpers
{
    public static class ImageHelper
    {
        public static string Image(this HtmlHelper helper, string id, string url, string alternateText)
        {
            return Image(helper, id, url, alternateText, null);
        }

        public static string Image(this HtmlHelper helper, string id, string url, string alternateText, object htmlAttributes)
        {
            // Instantiate a UrlHelper
            var urlHelper = new UrlHelper(helper.ViewContext.RequestContext);

            // Create tag builder
            var builder = new TagBuilder("img");

            // Create valid id
            builder.GenerateId(id);

            // Add attributes
            builder.MergeAttribute("src", urlHelper.Content(url));
            builder.MergeAttribute("alt", alternateText);
            builder.MergeAttributes(new RouteValueDictionary(htmlAttributes));

            // Render tag
            return builder.ToString(TagRenderMode.SelfClosing);
        }

    }
}

Notice that I create an instance of a URL helper like this:

// Instantiate a UrlHelper

var urlHelper = new UrlHelper(helper.ViewContext.RequestContext);

And then I can use the URL helper to resolve a URL like this:

// Add attributes

builder.MergeAttribute(“src”, urlHelper.Content(url));

Using UrlHelper.Content() does the same thing as Control.ResolveUrl(). I became curious about how the UrlHelper.Content() method worked internally. Because all of the the ASP.NET MVC framework source code is available to be downloaded from www.CodePlex.com, I was able to look up the implementation of the UrlHelper.Content() method quickly. It looks something like:

 

public string Content(string contentPath)
{
    if (string.IsNullOrEmpty(contentPath))
    {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "contentPath");
    }
    if (contentPath[0] == '~')
    {
        return VirtualPathUtility.ToAbsolute(contentPath, this.RequestContext.HttpContext.Request.ApplicationPath);
    }
    return contentPath;
}

Notice that the UrlHelper.Content() method does use the VirtualPathUtility class. So this has consequences for testability.

Discussion

  1. Farrio says:

    Thanks Stephen 🙂

  2. chandradev says:

    This is very nice code.

  3. hosein from iran says:

    thanks a lot for this article

  4. Murat says:

    nice to know. Thanks.

  5. Nirmal Shah says:

    Thanks Stephen…This will help.

  6. Evan Freeman says:

    So what does this do if you don’t supply the tilde? Return the relative pah you supply? Hmm interesting. I agree good tip.

  7. Bhupesh says:

    Thanks for your help

  8. Jana says:

    New joinee

  9. Marc Brooks says:

    I know this is completely aside from the point of the article, but why would an img tag _always_ need an id? In fact, when (rarely) would it need one?

  10. @Marc — Having an Id on an tag is useful when you need to refer to it in your Cascading Style Sheet file or JavaScript code. Sounds like you would want an overload of the Image() method without an id parameter which would only require three more lines of code 🙂

  11. Bob Armour says:

    Stephen,
    Interesting article – made even more interesting by the fact that I’ve just read an asp.net article (http://www.asp.net/learn/mvc/tutorial-35-cs.aspx) about Html helpers and was wondering why it didn’t resolve the URLs internally – to reduce repetition in the aspx pages.

    A bit of searching on the subject brought me to this page – imagine my surprise when I found that the code was not only almost identical to the orignal articel, but addressed the centralisation of URL resolution.

    I assume that the original tutorial was written before this page – so, how about updating the tutorial?

  12. Chris McDermott says:

    Stephen, you said “Notice that the UrlHelper.Content() method does use the VirtualPathUtility class. So this has consequences for testability”

    I’m migrating up from Preview 5 where an ImageHelper that I had written, like yours using the Url.Content, passed all my unit tests without a great deal of mocking or context setup. I’m now finding in v 1.0 that they are failing and Url.Content is returning an empty string.

    How have you unit tested your helper?

  13. rickj1 says:

    I have been looking for a Image control and code for displaying images from a data base with a GUID for UserId I ca get the image tag to show but not the image I’ve run out of angles any ideas
    thanks following your book up to chapter 6
    it is bringing everything into light
    still have a long way to go
    thanks agian

  14. Jason Monroe says:

    It should be noted, that VirtualPathUtility.ToAbsolute() will choke and die when trying to resolve a url with querystring parameters..

    In other words, something like this:
    ~/Handlers/Imagehander.ashx?id=432432

    when passed in will fail miserably.

    We use a custom PathHelper class to wrap VirtualPathUtility.. Basically, it splits off the query string params, translates the root of the url and re-appends the split query string

    public static class PathHelper
    {
    public static string ToAbsolute(string path, string root)
    {
    if (path.IndexOf(‘?’) == -1)
    path = VirtualPathUtility.ToAbsolute(path);
    else
    path = VirtualPathUtility.ToAbsolute(path.Substring(0, path.IndexOf(‘?’)), root) +
    path.Substring(path.IndexOf(‘?’));

    return path;
    }
    }

    So your ImageHelper’s Image() method could be written like this:

    public static string Image(this HtmlHelper helper, string id, string url, string alt, object htmlAttributes)
    {
    // Create our builder
    var img = new TagBuilder(“img”);
    img.GenerateId(id);
    // Apply our attributes
    img.MergeAttribute(“src”, PathHelper.ToAbsolute(url, helper.ViewContext.RequestContext.HttpContext.Request.ApplicationPath));
    img.MergeAttribute(“alt”, alt);
    img.MergeAttributes(new RouteValueDictionary(htmlAttributes));
    // return rendered tag
    return img.ToString(TagRenderMode.SelfClosing);
    }

  15. Mario says:

    Yeah,
    But I think time well give us throught the community the best helpers, for every situations

  16. ilahi dinle says:

    Thanks for your help

  17. müzik dinle says:

    thank coder web master

  18. Sohbet says:

    Thank web Master

  19. ilahi says:

    Yeah,
    But I think time well give us throught the community the best helpers, for every situations

  20. Really this is very nice stuff. Thanks for sharing.

  21. Good Stephen. Definitely the right move…

  22. Thank you for providing good information, I wish good work.

  23. feqr t I tried to mock a call to a Linq to SQL query, but I am struggling.

  24. müzik dinle says:

    Good Stephen. Definitely the right move…Thanks admin

  25. Thiwanka says:

    How add ‘class’ CSS value as attribute ?

    Html.Image(“phone”,”~/Content/Images/phone.gif”,”phone”, new {class=”phone”})

    not working since class is keyword

  26. ilahiler says:

    Good Stephen. Definitely the right move…Thanks admin thanks codr

  27. Ross MAson says:

    Ma49ny thanks. A Great tip!

  28. Delphi help says:

    Great helper for HTML users. Thanks.

  29. McCain says:

    Thanks for the tip. It will be of some help.
    Commercial Property Management Software | Dubai Short Stays

  30. It’s lucky e to know this, if it is really true. Companies tend not to realize when they create security holes from day-to-day operation.

  31. Great overview. Your style of writing is really a joy to read. http://www.mindestenshaltbar.net/0308/stories/1938