Telerik MVC Grid ActionLink column

The problem

Working with Telerik MVC Grid component, I ran into an issue where I wanted to add a column with an “Open” link. Clicking that link takes you to item’s editing form. It seems, however, that there’s no method to render a link columns by default.

You can achieve it, however, using template columns, something like this:

columns.Template(
	@<text>
		@Html.ActionLink("Open", "Edit", new { controller = "Items", id = item.ItemId })
	</text>
).ClientTemplate(@"<a href=""/Items/Edit?id=<#= ItemId #>"">Open</a>")

Note, that if you’re using AJAX binding in your grid, you have to specify a client template, that will be used by JavaScript. That template must be a constant string, but you can get bound item property values inserted in the template using ‘<#= PropertyName #>’ syntax.

It’s not very clean, however, because you have to supply two different templates to get the same HTML. It can lead you to stupid bugs if the generated HTML differs for server side and client side binding. That’s why I decided to write an extension method.

ActionLink extension method

What I wanted to achieve is this syntax, so we state what we want only once:

columns.ActionLink("Open", "Edit", (item) => new { id = item.ItemId})

We need to extend GridColumnFactory class for this:

public static GridTemplateColumnBuilder<T> ActionLink<T>(this GridColumnFactory<T> factory, string linkText, string action, string controller, Expression<Func<T, object>> routeValues)
		where T: class
	{
		//...
	}

For server side action link it’s all clear, we’re simply rendering an action link:

var urlHelper = new UrlHelper(factory.Container.ViewContext.RequestContext);

var builder = factory.Template(x =>
{
	var actionUrl = urlHelper.Action(action, controller, routeValues.Compile().Invoke(x));
	return string.Format(@"<a href=""{0}"">{1}</a>", actionUrl, linkText);
});

We invoke the routeValues expression, passing it the item that grid’s row is bound to. That allows us to pass row’s values as parameters to the action link.

Building client template is a bit harder. Since it used on the client side, we cannot invoke any C# code, meaning that we cannot execute routeValues expression. We can, however, build a template by parsing the expression:

if (!(routeValues.Body is NewExpression))
	throw new ArgumentException("routeValues.Body must be a NewExpression");

RouteValueDictionary routeValueDictionary = ExtractClientTemplateRouteValues(((NewExpression)routeValues.Body));

var link = urlHelper.Action(action, controller, routeValueDictionary);
var clientTemplate = string.Format(@"<a href=""{0}"">{1}</a>", link, linkText);

return builder.ClientTemplate(clientTemplate);

The key method here is ExtractClientTemplateRouteValues, that parses given NewExpression and builds a RouteDictionary containing route values. Method’s body is implemented like this:

private static RouteValueDictionary ExtractClientTemplateRouteValues(NewExpression newExpression)
{
	RouteValueDictionary routeValueDictionary = new RouteValueDictionary();

	for (int index = 0; index < newExpression.Arguments.Count; index++)
	{
		var argument = newExpression.Arguments[index];
		var member = newExpression.Members[index];

		object value;

		switch (argument.NodeType)
		{
			case ExpressionType.Constant:
				value = ((ConstantExpression) argument).Value;
				break;

			case ExpressionType.MemberAccess:
				MemberExpression memberExpression = (MemberExpression) argument;

				if (memberExpression.Expression is ParameterExpression)
					value = string.Format("<#= {0} #>", memberExpression.Member.Name);
				else
					value = GetValue(memberExpression);
				break;

			default:
				throw new InvalidOperationException("Unknown expression type!");
		}

		routeValueDictionary.Add(member.Name, value );
	}
	return routeValueDictionary;
}

The method walks through anonymous object’s property assignments and creates a map, either by getting constant or property values, or creating a client template parameter, if we have newExpression parameter’s property assigned. For example, if we pass an expression like this:

(item) => new { id = item.ItemId, param1 = 2 }

The dictionary will contain these items:

  • id: <#= ItemId #>
  • param1: 2

Note the format of the id value – it will be replaced with bound item’s ItemId property value.

Conclusion

I think the new extension method makes it easier to create action links for both, server side and client side bound grid. It’s shorter, cleaner and communicates the intent of the column much better.

Source code

Here’s the full source code of the class:

using System;
using System.Linq.Expressions;
using System.Web.Mvc;
using System.Web.Routing;
using Telerik.Web.Mvc.UI.Fluent;

namespace Telerik.Extensions
{
	public static class TelerikGridExtensions
	{
		public static GridTemplateColumnBuilder<T> ActionLink<T>(this GridColumnFactory<T> factory, string linkText, string action, Expression<Func<T, object>> routeValues)
			where T : class
		{
			return ActionLink(factory, linkText, action, string.Empty, routeValues);
		}

		/// <summary>
		/// Renders action links templates for both, server side and client side
		/// </summary>
		public static GridTemplateColumnBuilder<T> ActionLink<T>(this GridColumnFactory<T> factory, string linkText, string action, string controller, Expression<Func<T, object>> routeValues)
			where T: class
		{
			if (string.IsNullOrEmpty(controller))
				controller = factory.Container.ViewContext.Controller.GetType().Name.Replace("Controller", "");

			var urlHelper = new UrlHelper(factory.Container.ViewContext.RequestContext);

			var builder = factory.Template(x =>
			{
				var actionUrl = urlHelper.Action(action, controller, routeValues.Compile().Invoke(x));
				return string.Format(@"<a href=""{0}"">{1}</a>", actionUrl, linkText);
			});

			if (!(routeValues.Body is NewExpression))
				throw new ArgumentException("routeValues.Body must be a NewExpression");

			RouteValueDictionary routeValueDictionary = ExtractClientTemplateRouteValues(((NewExpression)routeValues.Body));

			var link = urlHelper.Action(action, controller, routeValueDictionary);
			var clientTemplate = string.Format(@"<a href=""{0}"">{1}</a>", link, linkText);

			return builder.ClientTemplate(clientTemplate);
		}

		private static RouteValueDictionary ExtractClientTemplateRouteValues(NewExpression newExpression)
		{
			RouteValueDictionary routeValueDictionary = new RouteValueDictionary();

			for (int index = 0; index < newExpression.Arguments.Count; index++)
			{
				var argument = newExpression.Arguments[index];
				var member = newExpression.Members[index];

				object value;

				switch (argument.NodeType)
				{
					case ExpressionType.Constant:
						value = ((ConstantExpression) argument).Value;
						break;

					case ExpressionType.MemberAccess:
						MemberExpression memberExpression = (MemberExpression) argument;

						if (memberExpression.Expression is ParameterExpression)
							value = string.Format("<#= {0} #>", memberExpression.Member.Name);
						else
							value = GetValue(memberExpression);

						break;

					default:
						throw new InvalidOperationException("Unknown expression type!");
				}

				routeValueDictionary.Add(member.Name, value );
			}
			return routeValueDictionary;
		}

		private static object GetValue(MemberExpression member)
		{
			var objectMember = Expression.Convert(member, typeof(object));
			var getterLambda = Expression.Lambda<Func<object>>(objectMember);
			return getterLambda.Compile().Invoke();
		}

	}
}

Advertisements

3 thoughts on “Telerik MVC Grid ActionLink column

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s