为解决ASP.NET MVC(CTP)中URL“页面请求”和“单纯逻辑处理请求”混淆问题,提供一条思路

   写在最前:本文主要是提供一种解决ASP.NET MVC(CTP)中URL“页面请求”和“单纯逻辑处理请求”混淆问题的思路,演示代码只作实现效果之用,不一定适合直接应用于“实战”,如有“粗燥”之处请多包涵。如果大家觉得可行,我们可以一起来完善她。

之前我很多次提到ASP.NET MVC 中“指令性”的URL,以及它可以给我们带来的一些新的体验,这样的URL可以把V层的页面逻辑(或者请求)让C层去承担,并且由C层负责判断到底将哪个网页最后传输到客户端。

这样的好处(或者说一部分的必要之处),是将VC在一定程度上分离开来,一切以Controller为中心,而不再是aspx。但是这样“指令性”的URL我感觉更像是一把双刃剑,我们说他好,也可以说它有很大缺陷

第一,比如当我们在一个Controller中的Action中的逻辑处理完毕之后,通常最后会有两种选择:RenderView(转向处理页面)或者RedirectToAction(转向另一个Action),如果你不这样做,而是像Web Forms或者ASP中那样下面不写点什么,那么返回的将是一个空白网页,至少不是你原来请求的入口网页。也就是说,一个Action只能返回相对固定的结果网页,除非你给他加上指定的参数或者每次都通过一系列判断。那如果我有很多地方要重用同一个Action,并且URL是不确定的,怎么办?

第二,由于MVC中请求的URL已经被Route过,并且通过一系列内部规则找到对应的Action才执行,当某个Action进行时,如果你想直接用Web Forms的方法得到请求网页的URLRequest.UrlReferrer),几乎是不可能的。这时候你用Request.URL找到的也不是你刚才在访问的网页URL,而是你主动请求的这个“指令性”URL。那么又出现一个问题,如果我想运行完一段Action后仍然回到这个网页,就像一个buttonpostback之后没有转向,那要如何做到呢?

基于这两点,我们当然可以有“水来土掩”的办法

第一,不重用Action,而是为许多的Action创建重用的方法,Action之后到底RenderView/RedirectToAction到什么地方,还是由Action各自负责。

第二,每个这样的Action执行完之后,用最“死”的办法指定返回的页面(也就是说一个页面的逻辑处理只对应一个Action)。

这样确实可以解决两个问题,但是他们都有各自的很大的不足

第一,这种方法使Action片断无畏的增加,这一切只是为了返回不同结果页而已。并且维护需要更加“深入”,不直观。同时这样做只会增加程序员体力和磁盘空间(说白了也就是“庞大”低效代码)的付出,不折不扣的“内耗”。

第二,同样的“内耗”。并且虽然没有涉及到较大的“重用”的需求,但是这些工作如果能像在Web Forms或者ASP中那样完成,岂不是更好。

 

为了同时解决这两个不足,我进行了几套方案的测试,并且最后确定了一套目前为止我所能想到的比较好的办法:把“指令性”URL“分流”,就是说指向同样的Controller中的同一个Action,通过一个页面上简单的参数,让他自动处理是返回请求页面还是继续。也就是说,把“页面请求”(不管是否需要逻辑处理,最后返回一个结果页)和“单纯逻辑请求”(就像我们很多时候用需要Web Forms中button做的那样)在需要的时候,彻底分离开,同时保证原有方法继续有效。

听上去似乎很容易实现,但是别忘了我有一些必要的前提:

1、能同时解决(或者改善)以上两个问题。

2、不破坏ASP.NET MVC本身的构架(也就是说不删除、修改任何代码,不Hack任何dll)。

3、尽量简单、高效,并且可能的话,考虑到改善其他安全性方面不足的问题。因为大家都知道,Web FormsPostBack方法可以很好的解决一些安全性方面的问题(执行逻辑的触发上),而目前MVCAction等于是全部暴露给用户的,既然要在URLAction上动手脚,那么看看是否可以把这个问题顺带处理掉(因为这方面涉及的问题比较多,我的案例中只是提供一种可行的思路,并不着力实现它)

 

当然,还要说明一下,这个未必是最好的办法,我这里只是提供一个可行的思路,这是我在开发一个测试ASP.NET MVC的项目过程中临时碰到和想到的,只是“逢山开路”了一下,可能还有更好的方法没有想到,或者一些细微处没有兼顾到,欢迎大家指出。如果大家有更好的,或者是因为本人对ASP.NET MVC认识不足而导致的“多此一举”也欢迎探讨!

 

下面介绍一下我的思路的一种做法:

我们要同时解决URLAction方面的问题,那么首先当然是要考虑到,如何获取到请求页的URL。既然Action不行,那么当然首先想到在Global.asax中指定(虽然这个办法有些“作孽”,不过因为只是测试,就先拿Global.asax开刀吧)。

我们在Global.asax中添加一套自己的URL标准格式:



           RouteTable.Routes.Add(
new Route

            
{

                Url 
= "[controller].mvc/[ao].html/[action]/[id]/[aop]",

                Defaults 
= new

                
{

                    action 
= "Index",

                    id 
= (string)null,

                    ao 
= "0",//(string)null

                    aop 
= (HttpContext.Current.Request.UrlReferrer != null?

                        Server.UrlEncode(HttpContext.Current.Request.UrlReferrer.PathAndQuery) : (
string)null

                }
,

                RouteHandler 
= typeof(MvcRouteHandler)

            }
);

 

其中[ao]Action Only的缩写,这个参数如果传入时不为0也不为空,那么就说明用户需要执行完后返回当前页,而不是View到其他地方去。

[aop]Action Only Page的缩写,这个字段用户不用传入,有服务器自动获得当前的URL(确切的说,对服务器来说是上一个),我们此处用HttpContext.Current.Request.UrlReferrer.PathAndQuery获取路径和参数。之所以不让用户在页面就传入URL,也不依靠外部数据,我是考虑到两点:一、简单易用;二、安全性,这样对于可以触发某些Action Only的网页,我们可以在第一时间有完全的控制(逻辑还没有执行的时候),我们可以把aop的赋值过程写入一个单独的过程,筛选触发这个Action的网页的URL(如Admin目录下指定URL), 当然我这里为了方便测试,没有展开。这样从根本上杜绝了外部的注入(因为直接在浏览器中打开是没有UrlReferrer的)和内部自定义链接注入(就像cnblogs中,用户自己用编辑器发一个链接作为“指令”)。

 

接下来我们需要有一个方法来接收处理[ao][aop]指令。

我写了一个ActionOnlyForControllers.cs放入Controller文件夹,因为Controller后面加了s,所以你不用担心V层的ActionOnlyFor之类文件夹的干扰:

 

using System;
using System.Web;
using System.Web.Mvc;

namespace ActionOnlyForControllers.Controllers
{
    
public class ActionOnlyForControllers
    
{
        
/// <summary>
        
/// 核对URL参数,确认是否需要ActionOnly
        
/// Check URL parameters([ao] and [aop]), make suer ACTIONONLY is hoped
        
/// </summary>
        
/// <param name="rd">RouteData</param>
        
/// <returns></returns>

        public static bool CheckAO(RouteData rd)
        
{
            
//确保有[ao]/[aop]参数
            
//check if [ao]/[aop] exist in the URL
            if (!rd.Values.ContainsKey("ao"|| !rd.Values.ContainsKey("aop"))
            
{
                
return false;
            }


            
//获取URL来源
            
//get the URL to return after ACTIONONLY
            string returnURL = (!string.IsNullOrEmpty(rd.Values["ao"].ToString()) && rd.Values["ao"].ToString() != "0" && rd.Values["aop"!= null?
                                  System.Web.HttpContext.Current.Server.UrlDecode(rd.Values[
"aop"].ToString()) : null;

            
//执行转向
            
//do Redirect
            if (!string.IsNullOrEmpty(returnURL))
            
{
                
//System.Web.HttpContext.Current.Response.Write(rd.Values["ao"].ToString() + "<br />");
                
//System.Web.HttpContext.Current.Response.Write(rd.Values["aop"].ToString() + "<br />");
                
//System.Web.HttpContext.Current.Response.Write(returnURL+"<br />");
                
//System.Web.HttpContext.Current.Response.Write(rd.Route.Url + "<br />");

                System.Web.HttpContext.Current.Response.Redirect(returnURL, 
true);
                
return true;
            }

            
else
            
{
                
//不转向,继续执行RenderView、Action等操作    
                
//[aop] isn't correct , return as nothing happened with "FALSE"
                return false;     
            }


        }

    }

}


 

过程上我都写了注释,大家如果发现有bug可以向我反映。

    这个判断我们直接在Controller中调用,方法如下:

       

[ControllerAction]
        
public void About()
        
{
            
// 以下Response.Write语句只做在V中确定这里曾经执行过逻辑的标记
            System.Web.HttpContext.Current.Response.Write("我在Controller中输出了一串字符");

            
//关键是这句
            if (!ActionOnlyForControllers.CheckAO(RouteData))

                RenderView(
"About");
        }

感谢一些朋友对这句话提出的一些看法:System.Web.HttpContext.Current.Response.Write("我在Controller中输出了一串字符");
    大家的说法我是同意的,但是这个语句并不是我在V里面输出数据的方法,这儿这么做只是为了能在页面执行后留下这段逻辑处理的“痕迹”。对应于我做的这个简单示例:ActionOnlyForControllers的一个简单示例 。 这里只是为了便于测试(因为只有这样不用牵扯到下游aspx文件),同样的效果大家可以把这句话理解为:
    ViewData["somewords"]="我在Controller中输出了一串字符";
    然后在aspx中调用 ViewData["somewords"]。
    谢谢!


    
只需要多加一行

if (!ActionOnlyForControllers.CheckAO(RouteData))

    可以实现“留在页面”或者“转向页面”的判断。

在HTML中我们只需要用一个ao参数来控制:ao=0或为空,则不执行判断,保持MVC原有形式。ao为其他值时,则执行完Action仍然回到本页面,其效果类似简单的postback一下。我们可以这样做:

    <%= Html.ActionLink("执行方法后回到本页面(button效果)"new { ao = 1, action = "About", controller = "Home" })%><br />
    
<%= Html.ActionLink("执行方法后继续执行RenderView(hyperlink效果)"new { ao = 0, action = "About", controller = "Home" })%></p>


    这样我们可以轻松的在“页面请求(+ 逻辑处理)”和“单纯逻辑处理”之间切换,让Action得到很好的重用,并且解决页面返回的问题。

当然,这些似乎看上去有点不太合ASP.NET MVC的本意,也许是我被Web Forms“宠”得太依赖于“postback体验”,但总之只要能为我们提供方便,并且达到特定的目的,MVC这个名字本身已经不再重要^_^

 

这里我提供了一个示例下载:ActionOnlyForControllers的一个简单示例
     友情提示:其中使用的MVCToolkit.dll是我曾经修改过的:MVC Toolkit 部分已发现bug的根治方案 Part(1) ,请不要在不知情的情况下当作官方的dll引用到其他项目中,以免出错。

posted on 2008-01-07 21:17  SZW  阅读(3216)  评论(23编辑  收藏  举报