随笔-19  评论-482  文章-1  trackbacks-13
   写在最前:本文主要是提供一种解决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引用到其他项目中,以免出错。

http://szw.cnblogs.com/
研究、探讨ASP.NET
转载请注明出处和作者,谢谢!

posted on 2008-01-07 21:17 SZW 阅读(1658) 评论(23)  编辑 收藏 所属分类: 原创ASP.NET MVC

评论:
#1楼  2008-01-08 08:36 | henry      
第一,不重用Action,而是为许多的Action创建重用的方法,Action之后到底RenderView/RedirectToAction到什么地方,还是由Action各自负责。

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

其实aspx作为v就已经具备httpcontext访问功能,在这种优势条件下Action可以完全做到和v只有数据输入和输出的处理,对于处理完成后应该去那个V应该是V自身的问题.但其怪的是MonoRail和asp.net mvc的设计都是把C和V紧绑定一起实在是想不明白.
  回复  引用  查看    
#2楼  2008-01-08 09:37 | bangbang [未注册用户]
博主可以考虑java中常用到的方法,做一个配置文件,用一个特定的字符串对应一个具体的URL,然后自己实现一个转向类,根据具体执行的结果里产生的字符串,寻找配置文件里对应的url,进行转向。
  回复  引用    
#3楼 [楼主] 2008-01-08 11:14 | SZW      
@henry
对于处理完成后应该去那个V应该是V自身的问题
================================
首先可以肯定的是,MVC是不提倡在aspx中进行复杂的逻辑处理的,就像你说的把C和V紧绑定一起不是个好办法,这些都是C应该处理的东西,理想状态下V最好只有输出(显示)和提交的功能,我不知道你对ASP.NET MVC进行了怎么样的测试,在这里面,处理完成之后的去向不是V决定的,而是C层的Action中的RenderView/RedirectToAction,V只是一个C层下的“终端”,至少官方的介绍目前都用了这样的方法。
对了,补充一点,你引用的两个方法都是我最后否定掉的,我要说的并不是用这两个办法,并且已经说明他们的不足:)

@bangbang
你用的方法不就是ASP.NET MVC默认的办法吗?只不过它写在了Global中,而不是配置文件里。我这里也正是在扩展这个方法阿。
  回复  引用  查看    
#4楼  2008-01-08 13:52 | henry      
@SZW
其实是规范问题,框架应该有更好的规范约束比较好,要不这样总的来说和webfrom差别不了多远.
System.Web.HttpContext.Current.Response.Write("我在Controller中输出了一串字符");
我觉得不应该把V表现的东西在c里能访问得到,其实很多时候没约束对不同使用的结果完全不一样.对于一些使用者在action中可以堆积比webfrom更可怕的代码出来.

  回复  引用  查看    
#5楼 [楼主] 2008-01-08 13:59 | SZW      
@henry
System.Web.HttpContext.Current.Response.Write("我在Controller中输出了一串字符");
写这个我只是为了在V层上表现出来我曾经在C层可以完成一些逻辑处理,并不在于实际需要在V上面输出这个字符串(不然我那个示例里面的对比很难从客户端直接看出来),V层的数据由ViewData从C层通过RenderView带到V层。不需要Response.Write。
我不明白你说的“不应该把V表现的东西在c里能访问得到”是什么意思,我文章里好像没有说到吧?
  回复  引用  查看    
#6楼 [楼主] 2008-01-08 14:01 | SZW      
@henry
还有你说的堆积Action的现象,目前默认的MVC框架是不可避免了,我做这个思考,让Action重用就是想尽量避免这样的情况。
  回复  引用  查看    
#7楼  2008-01-08 14:37 | 亚历山大同志      
@SZW
同意henry,C里头不必也不应该访问到HttpContext
  回复  引用  查看    
#8楼 [楼主] 2008-01-08 14:46 | SZW      
@亚历山大同志
你还不明白我上面为什么要写HttpContext.Current.Response.Write吗?
我不是为了访问页面的HttpContext,而是为了在测试结果的时候,能在页面上看到曾经这里执行过逻辑。HttpContext.Current.Response.Write在这里只是一个标记,不然你必须设断点去看。

我已经在原文中添加了一些说明,谢谢!
  回复  引用  查看    
#9楼  2008-01-08 15:16 | 亚历山大同志      
@SZW
我来的时候没看到,现在看到了。
  回复  引用  查看    
#10楼 [楼主] 2008-01-08 15:20 | SZW      
@亚历山大同志
谢谢你及时提醒,不然可能因为我表达的疏忽,又有很多朋友要误解了:)
  回复  引用  查看    
#11楼  2008-01-08 16:06 | henry      
@SZW
问题的是就现有的版本里controller的确集成了相关成员允许用户这么干.
我迷惑的只是这一点.
  回复  引用  查看    
#12楼 [楼主] 2008-01-08 16:20 | SZW      
@henry
对,如你所说,那样确实是不好的,因为V始终都应该是C的“下游”,不然就失去V和C分离的意义了(即使现在ASP.NET MVC还是有点“藕断丝连”的)。
但是如果只是HttpContext本身的话,我觉得到没有什么好奇怪的,毕竟MVC和Web Forms是同出一门,你只要能引用到system.web就行了,另外在C里面通过ControllerContext也可以访问到HttpContext,但是这不能说明一定就引用了V层的内容,只是提供了V层也可以完成的一些功能。另外保留了HttpContext也可能是出于对V-C数据协调问题还没有完善时候的一种补充吧。
并且通过我这个Response.write,其实可以更好地反映出MVC中C层和V层的一部分工作机制——RenderView更像是在Web Forms中Execute一个网页,原先对页面的操作都是保留的,也就是说这个空白网页在Action被访问时已经创建好了,剩下来的RenderView只是填充,而不是真正的“转向”。
  回复  引用  查看    
#13楼  2008-01-09 13:40 | 膘悍 [未注册用户]
你这是把简单问题复杂化。

谁说New.aspx的表单只能提交给Create.aspx,你还是可以像WebFrom那有提交给它自己,然后用if (Request.HttpMethod == "POST")判断是否要执行Create方法。

[ControllerAction]
public virtual void New()
{
if (this.Request.HttpMethod == "POST")
Create();
//RenderView("new"); //你可以自己测试下为什么这里的代码可以注释掉
}
public virtual void Create()
{
//处理代码
}
  回复  引用    
#14楼 [楼主] 2008-01-09 16:34 | SZW      
@膘悍
你这个能解决我说的什么问题呢?我又不是在说Form提交和URL直接提交Controller/Action上的区分。
况且你的代码只能指定到Create(),而我要实现的是什么地方来就返回什么地方,是不确定的,你这样的话这个Action还是解决不了我说的问题。

你上面的Create(); 也只是玩了一个文字游戏,和我说的RedirectAction("Create")实质上返回的逻辑效果是相似的,因为你弄到最后还是会涉及到RenderView/RedirectAction,你Create(); 里面不那么做的话,出来的将是一个空白网页,不信你可以自己试一下。所以那么做还是跑不出我说的缺陷。

还有你说的“谁说New.aspx的表单只能提交给Create.aspx”,我好像也没有这么说阿。


  回复  引用  查看    
#15楼  2008-01-10 11:35 | 膘悍 [未注册用户]
@SZW
我高估了你的能力,是我的错,我说的详细点。

你要解决的问题是“是什么地方来就返回什么地方,是不确定的”,我理清一下思路

一、假设我们当前所在的页面是/post/list.aspx

二、如果要返回当前页,那么就需要在进入/post/new.aspx之前传入当前路径的参数,所以list.aspx页面中的代码应该是<a href="/post/new.aspx?returnPath=<%= this.Requset.Path %>">新建</a>

你可能会说“我要到其他Controller的Action上,不是当前PostController的NewAction上”,这种情况你可以使用RouteTable.Routes.GetUrl方法得到你需要的Url。

三、在打开/post/new.aspx?returnPath=/post/list.aspx之后,用户要添加一个表单,提交给服务器处理。你的思路大概是提交给/post/create.aspx?returnPath=/post/list.aspx处理。

但这里有个问题,就是create.aspx可能会因为用户提交的表单不正确而返回/post/new.aspx?returnPath=/post/list.aspx。

这样问题就变成了如何返回/post/new.aspx?returnPath=list.aspx

如何返回?难道写成<form action="/post/create.aspx?returnPath=/post/new.aspx?returnPath=/post/list.aspx" method="post">吗?(这里的路径不进行转义的话本身就是有问题的)

为了避免这样的麻烦,最简单的办法就是提交给new.aspx自己,这样只要在new.aspx页面中写入<form method="post">就行了。

然后在new动作的最后调用这个函数

protected void RedirectToActionOrReturnPath(string actionName) {
string returnPath = Request.QueryString["returnPath"];

if (string.IsNullOrEmpty(returnPath) == false)
Response.Redirect(returnPath);
else
RedirectToAction(actionName);
}


我给出的代码中,Create方法上没有添加[ControllerAction],所以你说RedirectAction("Create")和在代码中直接调用的效果一样是错误的,你反编译下RedirectAction的代码看看吧。

一直看你在博客中发牢骚,还是多静下心来自己研究研究吧。
  回复  引用    
#16楼 [楼主] 2008-01-10 12:26 | SZW      
@膘悍
呵呵,我看到你的第二点已经有点不想看下去了,returnPath我在文中已经说了放弃这样的办法了,因为我不想让V去承担这些东西,那只会让V-C的数据处理的一些缺陷上会越陷越深(我做这个思考是有几个前提的,文中都已经写明了,特别是返回路径的问题,我需要让服务器自己获取)!你没看完就做这样断章取义的评论我觉得实在没什么好说的。

还有你说的[ControllerAction]我一开始就注意到了,所以我说特意说了“返回的逻辑效果是相似”而不是你所理解的“直接调用的效果一样”,不要曲解了我的意思。

另外我倒不觉得我是在发牢骚,只是在发表一些自己的思考,相反倒是有很多人对别的东西喜欢发牢骚
  回复  引用  查看    
#17楼  2008-01-10 12:28 | 膘悍 [未注册用户]
习惯重于配置,除了Login.aspx之外的其他操作几乎都是不需要指定returnPath的。
  回复  引用    
#18楼  2008-01-10 12:30 | 膘悍 [未注册用户]
@SZW
我来的时候没有看到你加下划线的部分。
  回复  引用    
#19楼 [楼主] 2008-01-10 12:31 | SZW      
@膘悍
--引用--------------------------------------------------
膘悍: 习惯重于配置,除了Login.aspx之外的其他操作几乎都是不需要指定returnPath的。
--------------------------------------------------------
这只能说明你的开发对象是这样,事实上很多系统对我所说的“单纯逻辑处理”的需求很多(你可以想象Web Forms中Postback但是不Response.Redirect的地方,当然还不止这些),如果这一点达不到,MVC的很多优势将很难发挥出来!
  回复  引用  查看    
#20楼 [楼主] 2008-01-10 12:32 | SZW      
@膘悍
--引用--------------------------------------------------
膘悍: @SZW
我来的时候没有看到你加下划线的部分。
--------------------------------------------------------
我第一次发布就这么做了,并且写在了“摘要”里
  回复  引用  查看    
#21楼  2008-01-10 13:11 | 留恋星空      
从小到大我们看过了很多文章,也写过很多文章,但大多已淡忘,唯有小学的阅读课本里的一篇文章我直到现在都记得,具体内容不清楚了,文章的名称是:“幸亏多看了一眼”。
  回复  引用  查看    
#22楼  2008-01-10 13:24 | 留恋星空      
不足与错误,两种说法,很多时候却是同一层含义,别人本着分享经验的初衷,写这个东西,肯定多少会有纰漏,因为毕竟每个人阅历存在不同,当然了,如果都一样了不也就没意思了吗?有时候是我们自己的眼睛蒙蔽了你,在没看清楚之前,切勿妄言。不然还有谁愿意说出自己的那个独特的东西呢?
  回复  引用  查看    
#23楼 [楼主] 2008-01-10 13:26 | SZW      
@留恋星空
呵呵,有点意思:)
今天正好有篇文章在讨论这个问题http://www.cnblogs.com/ThinkCode/archive/2008/01/10/1033215.html
  回复  引用  查看    

标题  
姓名