首页 | 新闻 | 交流 | 问吧 | 文档 | 手册 | 下载 | 博客

[原]ASP.NET MVC亲自指定Action参数值

作者:  时间: 2011-06-13

文章名字好难起哦,既想能清楚的表达本文的主旨,又想短小精悍,真难。

为啥要“亲自”呢?我想表达的意思是,在自已写的程序中自已控制一切,这就叫亲自。说起这个词,还有一个典故(真人真事,如果雷同,实属巧合):

在我上高三时,四班有一个位同学姓黄,名**,他以前在三中,后来转学到一中的。该黄姓同学一直在追求一种境界,到底是一种什么样的境界,很难描述...他可以在课堂上肆无忌惮的排放腹中废气,也可以在毫无征兆的情况下打个方圆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的数据