转载

Expression Tree 扩展MVC HtmlHelper 和 UrlHelper

表达式树是LINQ To everything 的基础,同时各种类库的Fluent API也 大量使用了Expression Tree。还记得我在不懂expression tree时,各种眼花缭乱的API 看的我各种膜拜,当我熟悉expression tree 后恍然大悟,不用看代码也能知道别人的API 是如何设计的(^_^)。 接下来这篇博客就谈谈如何使用expression tree扩展MVC中的HtmlHelper和UrlHelper。

场景

当我们在MVC中生成一个action的url会这样写:var url=UrlHelper.Action("index", "Home"); 如果要render一个action时会这样写:Html.RenderAction("index", "Home");

这样的写法瑕疵在于我们传递了两个字符串类型的参数在代码中,而我们又免不了对action和controller做重命名操作:index->default, 即便是你用resharper这样的工具重命名也无法将UrlHelper.Action("index", "Home"); 改变为UrlHelper.Action("default", "Home");

vs甚至在编译时都不会检查出来这个错误。 所以我们的目标是:设计出具有静态检查的API,让vs 提示出这个错误来,甚至是重命名时直接把相关代码都能重命名。

使用Expression Tree 重新设计这两组API

目标:设计出类似的API:Url.Action((HomeController c) => c.Index());

1.很明显我们需要在UrlHelper上写个扩展方法:

public static string Action<TController>(         this UrlHelper url,         Expression<Func<TController, ActionResult>> actionSelector,         string protocol = null,         string hostname = null)         {             var action = "Index"; //待解析             var controller = "Home";//带解析             var routeValues = new RouteValueDictionary();//待解析              return url.Action(action, controller, routeValues, protocol, hostname);         }

现在只需要根据表达式Expression<Func<TController, ActionResult>> actionSelector 解析出action,controller,还有routeValues即可

2.解析出controller 的名称

分析:controller的名称可以根据泛型方法中的泛型参数TController得到

private static string GetControllerName(Type controllerType)         {             var controllerName = controllerType.Name.EndsWith("Controller")                 ? controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length)                 : controllerType.Name;             return controllerName;         }

3.解析action的名称

分析:由于表达式Expression<Func<TController, ActionResult>> actionSelector 是一个MethodCallExpression, 所以可以很容易得到action的名称:var action=call.Method.Name;

这样已经完成了最简单的url构造方案, 但是我们还没有处理带有参数的action类型:Url.Action((HomeController c) => c.Detail(10, 20));

4.解析routeValues

分析:action中的参数实际上就时这个MethodCallExpression中的参数,我们解析这个expression的参数即可,然后得到RouteValues

private static RouteValueDictionary GetRouteValues(MethodCallExpression call) {  var routeValues = new RouteValueDictionary();  var args = call.Arguments;  ParameterInfo[] parameters = call.Method.GetParameters();  var pairs = args.Select((a, i) => new  {   Argument = a,   ParamName = parameters[i].Name  });  foreach (var argumentParameterPair in pairs)  {   string name = argumentParameterPair.ParamName;   object value = argumentParameterPair.Argument.GetValue();   if (value != null)   {    var valueType = value.GetType();    if (valueType.IsValueType)    {     routeValues.Add(name, value);    }    throw new NotSupportedException("unsoupported parameter type {0}".FormatWith(value.ToString()));   }  }  return routeValues; } 

如此一来,类似Url.Action((HomeController c) => c.Detail(10, 20));这样的action也可以构造出Url了, 目前这个解析对于传入复杂类型的action是不支持的,如下代码仅仅支持简单类型的参数:

if (valueType.IsValueType)       {           routeValues.Add(name, value);       }

对于action中传入复杂的类型:如传入一个类型为:

public class User     {         public int Age { get; set; }         public string Email { get; set; }     }

的参数到Action中,

public ActionResult SayHelloToUser(User user)         {             return new EmptyResult();         }

我们还需要继续解析这种类型的值到RouteValues中,

var properties = PropertyInfoHelper.GetProperties(valueType);    foreach (var propertyInfo in properties)      {         routeValues.Add(propertyInfo.Name, propertyInfo.GetValue(value));      }

大功告成,现在已经完美解决了各种类型的参数传入。

同样的道理,我们可以扩展HtmlHelper 的 RenderAction(), ActionLink()….

缺陷

早在09年,jeffery zhao就发表了lambda方式生成url的博客, 对比了几种方案的性能问题 ,并且给出了优化方案,当然,我在写这篇博客的时候还没有真正尝试去优化这个方案,只是再次拜读了大神的方案,记得早些年就读过这些文章,但是今天重新读过仍然获益匪浅,不由得感叹几句,莫非跑题了;-);-)

接下来我会思考这个优化的问题。

结束语:本文使用Expression tree 扩展了HtmlHelper和UrlHelper,给出了一个具有静态检查的API实现方式。 本文章所使用的源码提供下载 ,转载请注明出处

正文到此结束
Loading...