通过表达式树构建URL时正确识别ActionNameAttribute
2009-09-01 14:25 by 老赵, 5156 visits在MvcFutures项目中提供了一个辅助方法,可以将一个表达式树对象转化成一个RouteValueDictionary集合。只可惜,这个辅助方法的毛病比较多。例如,它直接把方法名作为action的值,而忽略了其上标记的ActionNameAttribute。这导致了某个被“改名”的Action方法一旦用在了表达式树中,最终得到的URL便是错误的。例如有一个Action方法:
public class HomeController : Controller { [ActionName("Default") public ViewResult Index() { ... } }
如果您使用这样的方式来生成URL(ActionEx方法的实现请参考《使用表达式树构建DomainRoute的URL》):
<a href="<%= Url.ActionEx<HomeController>(c => c.Index()) %>">Home</a>
则最终得到的代码是:
<a href="/Home/Index">Home</a>
而我们需要的结果应该是:
<a href="/Home/Default">Home</a>
正是因为这个原因(以及一些其他因素),许多朋友放弃使用强类型的方式构造URL。不过,如果您继续看下去,就会发现这个功能其实非常简单。只要做稍微一点点修改就可以了。不过现在,让我们来观察MvcFutures是如何实现这部分功能的。我已经把相关的代码复制到自己的RouteExpression类中:
public static class RouteExpression { public static RouteValueDictionary GetRouteValues<TController>( Expression<Action<TController>> action) where TController : Controller { if (action == null) { throw new ArgumentNullException("action"); } MethodCallExpression call = action.Body as MethodCallExpression; if (call == null) { throw new ArgumentException( "The action must be a method call.", "action"); } string controllerName = typeof(TController).Name; if (!controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException( "The controller name must end with 'Controller'.", "action"); } controllerName = controllerName.Substring( 0, controllerName.Length - "Controller".Length); if (controllerName.Length == 0) { throw new ArgumentException( "Cannot route to the Controller class", "action"); } var rvd = new RouteValueDictionary(); rvd.Add("controller", controllerName); rvd.Add("action", call.Method.Name); AddParameterValuesFromExpressionToDictionary(rvd, call); return rvd; } private static void AddParameterValues( RouteValueDictionary rvd, MethodCallExpression call) { ... } }
这段代码大部分内容都是进行参数校验,一旦出现以下情况之一,便会抛出异常:
- 表达式树为null。
- 表达式树不是一个MethodCallExpression(应该是一个Action方法的调用)
- 如果控制器类型的名称不以Controller结尾(破坏了约定)
- 如果控制器类型的名称就是Controller
经过校验之后,这个方法根据控制器类型的名称计算出controller(HomeController => Home),再把所调方法的名称作为action(Index() => Index)。最后,再使用AddParameterValues方法获得参数,并填充RouteValueDictionary(关于这点我们下次再来讨论)。
不过,问题就出现在从Action方法的MethodInfo“直接获取”名称这个步骤上。这个MethodInfo可能还标记着ActionNameAttribute呢,它的Name属性可不是action的名称。为此,我们必须多做这么一步:
private static ReaderWriterLockSlim s_rwLock = new ReaderWriterLockSlim(); private static Dictionary<MethodInfo, string> s_actionNames = new Dictionary<MethodInfo, string>(); private static string GetActionName(MethodInfo methodInfo) { string actionName = null; s_rwLock.EnterReadLock(); try { if (s_actionNames.TryGetValue(methodInfo, out actionName)) { return actionName; } } finally { s_rwLock.ExitReadLock(); } var attribute = (ActionNameAttribute)methodInfo .GetCustomAttributes(typeof(ActionNameAttribute), false) .SingleOrDefault(); actionName = attribute == null ? methodInfo.Name : attribute.Name; s_rwLock.EnterWriteLock(); try { s_actionNames[methodInfo] = actionName; } finally { s_rwLock.ExitWriteLock(); } return actionName; }
在GetActionName方法的中部则是获得action名称的代码。它会根据methodInfo上的ActionNameAttribute标记情况来确定。如果标记了ActionNameAttribute,则使用Attribute的Name属性作为action名称,否则就使用MethodInfo对象的Name属性。获得action名称之后,我们会将其保存在一个字典中。至于使用ReaderWriterLockSlim来控制并发读写的方式已经成为了标准,您甚至可以将其封装为一个组件避免重复编写相同的代码。
最后,我们把原来GetRouteValues方法中的一行代码加以替换即可:
public static RouteValueDictionary GetRouteValues<TController>( Expression<Action<TController>> action) where TController : Controller { ...rvd.Add("action", call.Method.Name);rvd.Add("action", GetActionName(call.Method)); ... }
ASP.NET MVC给了我们充分的自由度定制需要的组件。从中我们也可以了解到如何在项目中编写合适的API。其实很多东西只要多走一步就会美好很多,例如这个例子,需要花费您超过半小时的时间吗?
哇,抢了一个寂寞。