[原]ASP.NET MVC亲自指定Action参数值
时间:2011-06-13 来源:think8848
文章名字好难起哦,既想能清楚的表达本文的主旨,又想短小精悍,真难。
为啥要“亲自”呢?我想表达的意思是,在自已写的程序中自已控制一切,这就叫亲自。说起这个词,还有一个典故(真人真事,如果雷同,实属巧合):
在我上高三时,四班有一个位同学姓黄,名**,他以前在三中,后来转学到一中的。该黄姓同学一直在追求一种境界,到底是一种什么样的境界,很难描述...他可以在课堂上肆无忌惮的排放腹中废气,也可以在毫无征兆的情况下打个方圆30m之内其他房间可以听到的喷嚏,甚至可以和校长开玩笑,一次,他课间去嘘嘘,本来是不允许学生去教师的WC的,但他是无视这种规定的,他先到的,正在放水时校长进来了,他就和校长打了个招呼:“高校长,您亲自来上厕所了?”,校长被憋的竟只得"唔,唔"搪塞过去...
很是怀念校园的时光啊
进入正题:
为啥需要“亲自”呢,这得说明来龙去脉:我的本意是做一个RESTful服务,自已写了一个Atom的Client,使用WebRequest向服务器提交数据,当然,格式是Atom10的,在服务器端使用Request把Client传上来的数据拿出来,如下伪代码所示:
public ActionResult Create() { var entry = GetFromRequest(); //... }
这样好么?刚写好时觉得不错,但第二眼就觉得不好了,为啥,如果这个entry是从Create方法的参数中传来的该多好啊,想起以前在学习WCF时遇到的一个问题,使用Atom10FeedFormatter类就可以在参数中获得这个entry的实例了,何乐不为呢,于是代码变成这样:
public ActionResult Create(Atom10FeedFormatter<LogEntry> log) { //... }
不幸的是,按照之前经验,在该方法内部可以通过log.Item就获得从Client传来的实体了,但是在这里发现log.Item居然为null。
这才发现原来MVC和WCF Syndication的机制是不同的啊。
通过阅读MVC的源代码发现,ControllerActionInvoker中一个方法GetParameterValues,这个方法获取Action的参数列表,然后把根据一些策略生成对应的参数值,但是这个GetParameterValues似乎只能通过重载才能达到我想要的目的,达到我的目的又如何保证不影响MVC本来的意图呢?还是先试试不重载,看有没有轻量级的解决方案吧。
又想起来MVC中Filter不是几乎无所不能么,要么来个ActionFitler好了,于是定义了一个类型:
[AttributeUsage(AttributeTargets.Method)] public class AtomEntryConvertAttribute : Attribute, IActionFilter { public void OnActionExecuting(ActionExecutingContext filterContext) { //... } public void OnActionExecuted(ActionExecutedContext filterContext) { } }
试图在OnActionExecuting中从Request中读取数据,结果很快发现了问题,Action都执行了,该特性类的方法还没有执行,仔细查看MVC的源代码,发现了问题所在:
protected virtual ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters) { ActionExecutingContext preContext = new ActionExecutingContext(controllerContext, actionDescriptor, parameters); Func<ActionExecutedContext> continuation = () => new ActionExecutedContext(controllerContext, actionDescriptor, false /* canceled */, null /* exception */) { Result = InvokeActionMethod(controllerContext, actionDescriptor, parameters) }; // need to reverse the filter list because the continuations are built up backward Func<ActionExecutedContext> thunk = filters.Reverse().Aggregate(continuation, (next, filter) => () => InvokeActionMethodFilter(filter, preContext, next)); return thunk(); }
Action的调用在ActionMethodFitler之前...汗啊,真是不长记性,上次就在这里被“骗”一次,时间不长居然又忘了这茬了。
又回到GetParameterValues方法...
(此处省去2小时的思考,尝试过程)
有一点结论了,如果要实现轻量级“非侵入”式的操作,以M$的习惯,一般是使用Attribute或者反射的,这里用反射似乎不妥,那么就应该在Entry类型或是参数本身上应用Attribute比较合适,根据M$的命名习惯,这种事情应该使用诸如Custom、Convert、Parameter之类的名称,带着这个思路一找,果然发现了一个类型CustomModelBinderAttribute。
光是这个类型名,就感觉是干这个事情的,自定义 模型 绑定 特性 ,看起来像了,该类型中有一个公共方法:
public abstract IModelBinder GetBinder();
刚才在读MVC源代码时就发现这个IModelBinder接口大量使用在GetParameterValues方法中,差不多了,再看IModelBinder的定义:
object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
看定义就觉得像是从controllerContext和bindingContext中生成一个参数值的。
直接创建一个CustomModelBinderAttribute的子类:
[AttributeUsage(AttributeTargets.Parameter)] public class AtomEntryParameterConvertAttribute : CustomModelBinderAttribute { public Type EntryType { get; private set; } public AtomEntryParameterConvertAttribute(Type entryType) :base() { this.EntryType = entryType; } public override IModelBinder GetBinder() { return new AtomEntryConvertModelBinder(this.EntryType); } }
再创建一个IModelBinder的实现:
internal class AtomEntryConvertModelBinder : IModelBinder { public Type EntryType { get; private set; } internal AtomEntryConvertModelBinder(Type entryType) { this.EntryType = entryType; } public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var result = AtomServiceHelper.GetDataFromEntry(controllerContext.HttpContext.Request, this.EntryType, bindingContext.ModelType); return result; } }
这里我要稍说明下,其实我在Action的参数中收到的不是直接从客户端上传来的SyndicationItem类型的子类AtomEntry,AtomEntry只是Client和Server交互时的基于Atom协议的数据结构,它派生自SyndicationItem类,真正在Server上使用的是实体类,在我的例子中:
public class Log {}
Log类型是真正服务端业务层、数据层使用的实体类型
public class LogEntry : AtomEntry {} public class AtomEntry : SyndicationItem {}
LogEntry类型是Client与Server交互的数据格式
因此,我不仅需要从Client上把LogEntry的实例传到Server,而且还要在Server上的Action中使用参数直接获得Log类型的实例,当然Log和LogEntry的定义是一个策略,或者说它们之间是有约定的,没有约定,它俩也完不成转换,我喜欢约定甚于配置。
AtomServiceHelper.GetDataFromEntry(controllerContext.HttpContext.Request, this.EntryType, bindingContext.ModelType)
正是AtomServiceHelper类读取Request中的数据,然后将EntryType类型的实例转换为参数类型的实例。
其实对于IModuleBinder我也没有查看MSDN,只是感觉就是这种用法,对不对还没有做实验,于是写个例子:
[HttpPost] [ServiceError] public ActionResult Create([AtomEntryParameterConvert(typeof(LogEntry))]LogRecord log) { var logMng = new LogManager(); logMng.CreateLog(log); return new EmptyResult(); }
运行,yeah!果然如愿得到了来自Client的数据