Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add to htmlAttributes for custom ActionLink helper extension

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.

like image 993
wysinawyg Avatar asked Jan 17 '14 14:01

wysinawyg


2 Answers

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.
like image 77
slawek Avatar answered Oct 07 '22 14:10

slawek


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!

like image 39
Daniel J.G. Avatar answered Oct 07 '22 13:10

Daniel J.G.