I'm trying to create a simple custom version of the Html.ActionLink(...) HtmlHelper
I want to append a set of extra attributes to the htmlAttributes annonymous object passed in.
public static MvcHtmlString NoFollowActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
{
var customAttributes = new RouteValueDictionary(htmlAttributes) {{"rel", "nofollow"}};
var link = htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, customAttributes);
return link;
}
So in my view I would have this:
@Html.NoFollowActionLink("Link Text", "MyAction", "MyController")
Which I'd expect to render out a link like:
<a href="/MyController/MyAction" rel="nofollow">Link Text</a>
But instead I get:
<a href="/MyController/MyAction" values="System.Collections.Generic.Dictionary`2+ValueCollection[System.String,System.Object]" keys="System.Collections.Generic.Dictionary`2+KeyCollection[System.String,System.Object]" count="1">Link Text</a>
I've tried various methods of converting the annonymous type into a RouteValueDictionary, adding to it then passing that in to the root ActionLink(...) method OR converting to Dictionary, OR using HtmlHelper.AnonymousObjectToHtmlAttributes and doing the same but nothing seems to work.
The result you get is caused by this source code :
public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
{
return ActionLink(htmlHelper, linkText, actionName, controllerName, TypeHelper.ObjectToDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
As you can see HtmlHelper.AnonymousObjectToHtmlAttributes
is called inside. That's why you get values
and keys
when you pass it RouteValueDictionary
object.
There are only two methods matching your argument list:
public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
The second overload doesn't do anything to your parameters, just passes them.
You need to modify your code to call the other overload:
public static MvcHtmlString NoFollowActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues = null, object htmlAttributes = null)
{
var routeValuesDict = new RouteValueDictionary(routeValues);
var customAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
if (!customAttributes.ContainsKey("rel"))
customAttributes.Add("rel", "nofollow");
return htmlHelper.ActionLink(linkText, actionName, controllerName, routeValuesDict, customAttributes);
}
When you pass routeValues
as RouteValueDictionary
the other overload is picked (RouteValueDictionary
is implementing IDictionary<string, object>
so it's fine), and the returned link is correct.
If the rel
attribute will be already present in htmlAttributes
object, an exception will be thrown:
System.ArgumentException: An item with the same key has already been added.
As you want to update the htmlAttributes object received so you can add a new attribute (rel), you will need to convert the anonymous htmlAttributes object into an IDictionary<string,object>
(As you cannot add new properties to the anonymous object).
That means you will need to call this overload of the ActionLink
method, which also requires the anonymous routeValues
to be converted as a RouteValueDictionary
.
You can easily convert the route values using new RouteValueDictionary(routeValues)
. For converting the html Attributes you will need some reflection logic, for example as in this question. (As already mentioned by slawek in his answer, you could also take advantage that RouteValueDictionary
already implements a dictionary and convert the htmlAttributes in the same way)
In the end your extension would be something like this:
public static MvcHtmlString NoFollowActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues = null, object htmlAttributes = null)
{
var htmlAttributesDictionary = new Dictionary<string, object>();
if (htmlAttributes != null)
{
foreach (var prop in htmlAttributes.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
htmlAttributesDictionary.Add(prop.Name, prop.GetValue(htmlAttributes, null));
}
}
htmlAttributesDictionary["rel"] = "nofollow";
var routeValuesDictionary = new RouteValueDictionary(routeValues);
var link = htmlHelper.ActionLink(linkText, actionName, controllerName, routeValuesDictionary, htmlAttributesDictionary);
return link;
}
If you call it like this:
@Html.NoFollowActionLink("Link Text", "MyAction", "MyController", new { urlParam="param1" }, new { foo = "dummy" })
You will get the following html:
<a foo="dummy" href="/MyController/MyAction?urlParam=param1" rel="nofollow">Link Text</a>
Note because it is adding/updating the rel attribute after adding the original attributes into the dictionary, it will always force rel="nofollow"
even when the caller specifies another value for the attribute.
Hope it helps!