Click here to Skip to main content
15,886,518 members
Articles / Programming Languages / C#

Telerik MVC Grid ActionLink Column

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
21 Jul 2011CPOL2 min read 46.1K   3   3
Teleric MVC Grid ActionLink Column

This article appears in the Third Party Products and Tools section. Articles in this section are for the members only and must not be used to promote or advertise products in any way, shape or form. Please report any spam or advertising.

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:

C#
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:

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

We need to extend GridColumnFactory class for this:

C#
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:

C#
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 is 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:

C#
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:

C#
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:

C#
(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:

C#
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();
		}
	}
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Lithuania Lithuania
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionlinkText as Paramater Pin
gr1ngo17-Nov-11 6:18
gr1ngo17-Nov-11 6:18 
AnswerRe: linkText as Paramater Pin
Gediminas Geigalas17-Nov-11 6:36
Gediminas Geigalas17-Nov-11 6:36 
GeneralRe: linkText as Paramater Pin
gr1ngo18-Nov-11 5:43
gr1ngo18-Nov-11 5:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.