Actionlinks with html

Published on den 10 December 2009

While building this blog there where a few places where I needed images or html inside my link tag. The ActionLink in ASP.NET MVC does not include support for this. Because of that I decided to to use plain html links. That worked well untill I had to deploy to IIS6 and change all my routes which broke all the plain html links. Here is a better approach.

The goal

My goal was to be able to use the ActionLink functionality in MVC so that I could change my routes and the links would update. But I also had a need to support any kind of html in the link. What I wanted was something like this

  <%= Html.HtmlActionLink("<span class="name">" + item.Name + " <span class="postCount">" + item.Count.ToString() + "", "ByTag", "Archive", new) %>

And I also wanted all the overload available for ActionLink to be Available for HtmlActionLink

My options

The problem with ActionLink is that it encodes any string I put in as text. One very simple solution is to create a wrapper around ActionLink and put in some dummy value into ActionLink and in the returned html replace the values with the content I wanted to have. I don't really like the fact that I produce a string and then transform it.

Another solution would be to implement the methods and then build the tags by hand using the TagBuilder class. At first I thought this would be the approach I would use but when I looked at the ActionLink class in the MVC source which you can get at codeplex I decided it was too many overloads and to much options to handle.

Remember KISS! There is more important things to spend my time on then that so I decided to go for approach one which is dead simple to implement.

Implementation

The first thing I did was to find the class that included the ActionLink extension methods in the source code for the framework. It is found in LinkExtensions.cs and looks like this

public static class LinkExtensions {
	public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName) {
	    return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, new RouteValueDictionary(), new RouteValueDictionary());
	}

	public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, object routeValues) {
	    return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, new RouteValueDictionary(routeValues), new RouteValueDictionary());
	}

	public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, object routeValues, object htmlAttributes) {
	    return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, new RouteValueDictionary(routeValues), new RouteValueDictionary(htmlAttributes));
	}

	public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, RouteValueDictionary routeValues) {
	    return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, routeValues, new RouteValueDictionary());
	}

	public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) {
	    return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, routeValues, htmlAttributes);
	}

	public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName) {
	    return ActionLink(htmlHelper, linkText, actionName, controllerName, new RouteValueDictionary(), new RouteValueDictionary());
	}

	public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes) {
	    return ActionLink(htmlHelper, linkText, actionName, controllerName, new RouteValueDictionary(routeValues), new RouteValueDictionary(htmlAttributes));
	}

	public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) {
	    if (String.IsNullOrEmpty(linkText)) {
	        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
	    }
	    return MvcHtmlString.Create(HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, null/* routeName */, actionName, controllerName, routeValues, htmlAttributes));
	}

	public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, object routeValues, object htmlAttributes) {
	    return ActionLink(htmlHelper, linkText, actionName, controllerName, protocol, hostName, fragment, new RouteValueDictionary(routeValues), new RouteValueDictionary(htmlAttributes));
	}

	public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) {
	    if (String.IsNullOrEmpty(linkText)) {
	        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
	    }
	    return MvcHtmlString.Create(HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, null /* routeName */, actionName, controllerName, protocol, hostName, fragment, routeValues, htmlAttributes));
	}

	//Only route links from here, which I did not implement

}

As you see there are plenty of overloads here but really only two method where links are created. So for me it was only needed to change two methods and then change the method names. Here you can see my implementation

public static class ImageLinkHelper
    {
        public static MvcHtmlString HtmlActionLink(this HtmlHelper htmlHelper, string linkHtml, string actionName)
        {
            return HtmlActionLink(htmlHelper, linkHtml, actionName, null /* controllerName */, new RouteValueDictionary(), new RouteValueDictionary());
        }

        public static MvcHtmlString HtmlActionLink(this HtmlHelper htmlHelper, string linkHtml, string actionName, object routeValues)
        {
            return HtmlActionLink(htmlHelper, linkHtml, actionName, null /* controllerName */, new RouteValueDictionary(routeValues), new RouteValueDictionary());
        }

        public static MvcHtmlString HtmlActionLink(this HtmlHelper htmlHelper, string linkHtml, string actionName, object routeValues, object htmlAttributes)
        {
            return HtmlActionLink(htmlHelper, linkHtml, actionName, null /* controllerName */, new RouteValueDictionary(routeValues), new RouteValueDictionary(htmlAttributes));
        }

        public static MvcHtmlString HtmlActionLink(this HtmlHelper htmlHelper, string linkHtml, string actionName, RouteValueDictionary routeValues)
        {
            return HtmlActionLink(htmlHelper, linkHtml, actionName, null /* controllerName */, routeValues, new RouteValueDictionary());
        }

        public static MvcHtmlString HtmlActionLink(this HtmlHelper htmlHelper, string linkHtml, string actionName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
        {
            return HtmlActionLink(htmlHelper, linkHtml, actionName, null /* controllerName */, routeValues, htmlAttributes);
        }

        public static MvcHtmlString HtmlActionLink(this HtmlHelper htmlHelper, string linkHtml, string actionName, string controllerName)
        {
            return HtmlActionLink(htmlHelper, linkHtml, actionName, controllerName, new RouteValueDictionary(), new RouteValueDictionary());
        }

        public static MvcHtmlString HtmlActionLink(this HtmlHelper htmlHelper, string linkHtml, string actionName, string controllerName, object routeValues, object htmlAttributes)
        {
            return HtmlActionLink(htmlHelper, linkHtml, actionName, controllerName, new RouteValueDictionary(routeValues), new RouteValueDictionary(htmlAttributes));
        }

        public static MvcHtmlString HtmlActionLink(this HtmlHelper htmlHelper, string linkHtml, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
        {
            if (String.IsNullOrEmpty(linkHtml))
            {
                throw new ArgumentException("Null or empty", "linkHtml");
            }
            string link = HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, "#%#%", null/* routeName */, actionName, controllerName, routeValues, htmlAttributes);

            return MvcHtmlString.Create(link.Replace("#%#%", linkHtml));
        }

        public static MvcHtmlString HtmlActionLink(this HtmlHelper htmlHelper, string linkHtml, string actionName, string controllerName, string protocol, string hostName, string fragment, object routeValues, object htmlAttributes)
        {
            return HtmlActionLink(htmlHelper, linkHtml, actionName, controllerName, protocol, hostName, fragment, new RouteValueDictionary(routeValues), new RouteValueDictionary(htmlAttributes));
        }

        public static MvcHtmlString HtmlActionLink(this HtmlHelper htmlHelper, string linkHtml, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
        {
            if (String.IsNullOrEmpty(linkHtml))
            {
                throw new ArgumentException("Null or empty", "linkHtml");
            }
            string link = HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, "#%#%", null /* routeName */, actionName, controllerName, protocol, hostName, fragment, routeValues, htmlAttributes);
            return MvcHtmlString.Create(link.Replace("#%#%", linkHtml));
        }

    }

After adding this class to my project I could replace my pure html links with ActionLinks containing whatever html I wanted. All in all this took about 10 minutes or so including investigating my options.

Room for improvement

I have not investigated this yet but it seems likely to me that you could improve the perforamance by not doing a replace but rather take a more manual approach. string.Replace will look through the whole string for parts that should be replaced, in this case we know that our string sits just after the first >. An example

<a href="#"><span style="color: rgb(255, 0, 0);">#%#%</span> ..........

I'm thinking something like stringBefore + linkHtml + stringAfter. I just did not feel the need for it. Maybe I will upgrade the solution later.

Summary

This was one of the problems I faced when deplyoing the site. The solution was simple enough and I wish I had fixed it right away when I was developing the site. If you use ActionLink instead of plain html links it gracefully handles if your routes change.

The next post will be about how to make Linq-To-Sql's DataContext pick the correct connectionstring in a multiproject solution.

Then feel free to it or if you have any comments or questions mention @MikaelEliasson on Twitter.

CTO and co-founder at Bokio with a background as an elite athlete. Still doing a lot of sports but more for fun.

#development, #web, #orienteering, #running, #cycling, #boardgames, #personaldevelopment