Updated: 3/7/2011
So it’s been a while. I have a new job working for Headspring in Austin, which is one of the reasons I haven’t blogged in a while. It’s tough moving and re-adjusting to a new area and job, but Austin is great and Headspring is terrific. I started blogging on the Headspring site too. I’m trying to alternate weeks for posting, so we’ll see how that goes. (also fyi, Headspring is hiring)
On to the good stuff, I started playing with an HtmlHelper extension to rid a project of magic strings for action links. It was definitely more tricky than I originally thought, but I have a first attempt ready to share. Here’s the code:
@(Html.ActionLinkFor<HomeController>(a=>a.Contact(Model), "Contact"
, new Dictionary<string, object>{{"style","color: #f90;background:#000"}})) <br />
@(Html.ActionLinkFor<MainControllerController>(a=>a.Index(), "Main Home"))<br/>
@(Html.ActionLinkFor<HomeController>(a=>a.Index(), "Home")) <br />
@(Html.ActionLinkFor<HomeController>(a=>a.AnotherTest(new SampleModel{Id=5555
, Name = "Just a test", Address = "123 Main"}), "Another Test"))<br/>
@(Html.ActionLinkFor<HomeController>(a=>a.TestYetAnother(new SampleModel{Id=12345678
, Name = "Just another test yet"}), "Yet Another Test"))
The code above is what I wanted for the end result. The code below is what makes the code above work.
public static class HtmlHelperExtensions
{
public static IHtmlString ActionLinkFor<TController>(this HtmlHelper htmlHelper
, Expression<Func<TController, object>> expression,
string displayText) where TController : Controller
{
return ActionLinkFor(htmlHelper, expression, displayText, null);
}
public static IHtmlString ActionLinkFor<TController>(this HtmlHelper htmlHelper
, Expression<Func<TController, object>> expression, string displayText
, IDictionary<string, object> htmlAttributes)
where TController : Controller
{
var method = ((MethodCallExpression)expression.Body).Method;
var actionName = method.Name;
var parameters = method.GetParameters();
var arguments = ((MethodCallExpression)expression.Body).Arguments;
var routeValue = new RouteValueDictionary();
for (int i = 0; i < parameters.Length; i++)
{
if (arguments[i] as MemberExpression != null)
{
var memberExpression = (MemberExpression)arguments[i];
if (memberExpression.Expression as ConstantExpression == null)
AddRouteValuesForMemberExpression((MemberExpression)arguments[i]
, parameters[i].Name, routeValue);
if (memberExpression.Expression as ConstantExpression != null)
AddRouteValuesForConstantExpression(((MemberExpression)arguments[i])
, parameters[i].Name, routeValue);
}
if (arguments[i] as MemberInitExpression != null)
AddRouteValuesForMemberInitExpression((MemberInitExpression)arguments[i]
, routeValue);
if (arguments[i] as MethodCallExpression != null)
AddRouteValuesForMethodCallExpression((MethodCallExpression)arguments[i]
, parameters[i].Name, routeValue);
}
var controller = typeof(TController).Name;
var controllerName = controller.Remove(controller.LastIndexOf("Controller"));
return htmlHelper.ActionLink(displayText, actionName, controllerName, routeValue, htmlAttributes);
}
private static void AddRouteValuesForMethodCallExpression(MethodCallExpression methodCallExpression
, string parameterName, RouteValueDictionary routeValue)
{
if (methodCallExpression.Object != null)
{
var value = ((PropertyInfo)((MemberExpression)methodCallExpression.Object).Member)
.GetValue(((ConstantExpression)((MemberExpression)methodCallExpression.Object)
.Expression).Value, null);
routeValue.Add(parameterName, value);
}
}
private static void AddRouteValuesForMemberExpression(MemberExpression memberExpression
, string parameterName, IDictionary<string, object> routeValue)
{
object model;
if (((MemberExpression)memberExpression.Expression).Member is PropertyInfo)
model = ((PropertyInfo)((MemberExpression)memberExpression.Expression).Member)
.GetValue(((ConstantExpression)((MemberExpression)memberExpression.Expression)
.Expression).Value, null);
else
model = ((FieldInfo)((MemberExpression)memberExpression.Expression).Member)
.GetValue(((ConstantExpression)((MemberExpression)memberExpression.Expression)
.Expression).Value);
foreach (var p in model.GetType().GetProperties())
{
if (p.Name == memberExpression.Member.Name)
{
var value = p.GetValue(model, null);
if (value != null && !routeValue.ContainsKey(p.Name))
routeValue.Add(parameterName, value.ToString());
}
}
}
private static void AddRouteValuesForConstantExpression(MemberExpression memberExpression
, string parameterName, IDictionary<string, object> routeValue)
{
var properties = ((PropertyInfo)memberExpression.Member)
.GetValue(((ConstantExpression)memberExpression.Expression).Value, null)
.GetType()
.GetProperties();
foreach (var property in properties)
{
var value = property.GetValue(((PropertyInfo)memberExpression.Member)
.GetValue(((ConstantExpression)memberExpression.Expression).Value, null), null);
if (value != null)
routeValue.Add(property.Name, value.ToString());
}
if (properties.Length == 0)
{
var value = ((PropertyInfo)memberExpression.Member)
.GetValue(((ConstantExpression)memberExpression.Expression).Value, null);
if (value != null)
routeValue.Add(parameterName, value.ToString());
}
}
private static void AddRouteValuesForMemberInitExpression(MemberInitExpression memberInitExpression
, IDictionary<string, object> routeValue)
{
foreach (var p in memberInitExpression.Bindings.Cast<MemberAssignment>())
{
if (p.Expression is ConstantExpression)
{
var value = ((ConstantExpression)p.Expression).Value;
var name = p.Member.Name;
if (value != null)
routeValue.Add(name, value.ToString());
}
if (p.Expression is UnaryExpression)
{
throw new NotImplementedException("Not able to handle UnaryExpressions yet.");
}
}
}
}
Basically the way this thing works is off the Expression<Func<TController, object>> parameter being passed into the ActionLinkFor method. From the expression, you can determine the route values and action name…by doing a lot of manipulation. So, if you would, try this helper out and let me know of any bugs or improvements that can be made.