为解决ASP.NET MVC(CTP)中URL“页面请求”和“单纯逻辑处理请求”混淆问题,提供一条思路
之前我很多次提到ASP.NET MVC 中“指令性”的URL,以及它可以给我们带来的一些新的体验,这样的URL可以把V层的页面逻辑(或者请求)让C层去承担,并且由C层负责判断到底将哪个网页最后传输到客户端。
这样的好处(或者说一部分的必要之处),是将V和C在一定程度上分离开来,一切以Controller为中心,而不再是aspx。但是这样“指令性”的URL我感觉更像是一把双刃剑,我们说他好,也可以说它有很大缺陷:
第一,比如当我们在一个Controller中的Action中的逻辑处理完毕之后,通常最后会有两种选择:RenderView(转向处理页面)或者RedirectToAction(转向另一个Action),如果你不这样做,而是像Web Forms或者ASP中那样下面不写点什么,那么返回的将是一个空白网页,至少不是你原来请求的入口网页。也就是说,一个Action只能返回相对固定的结果网页,除非你给他加上指定的参数或者每次都通过一系列判断。那如果我有很多地方要重用同一个Action,并且URL是不确定的,怎么办?
第二,由于MVC中请求的URL已经被Route过,并且通过一系列内部规则找到对应的Action才执行,当某个Action进行时,如果你想直接用Web Forms的方法得到请求网页的URL(Request.UrlReferrer),几乎是不可能的。这时候你用Request.URL找到的也不是你刚才在访问的网页URL,而是你主动请求的这个“指令性”URL。那么又出现一个问题,如果我想运行完一段Action后仍然回到这个网页,就像一个button被postback之后没有转向,那要如何做到呢?
基于这两点,我们当然可以有“水来土掩”的办法:
第一,不重用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 Forms的PostBack方法可以很好的解决一些安全性方面的问题(执行逻辑的触发上),而目前MVC的Action等于是全部暴露给用户的,既然要在URL和Action上动手脚,那么看看是否可以把这个问题顺带处理掉(因为这方面涉及的问题比较多,我的案例中只是提供一种可行的思路,并不着力实现它)
当然,还要说明一下,这个未必是最好的办法,我这里只是提供一个可行的思路,这是我在开发一个测试ASP.NET MVC的项目过程中临时碰到和想到的,只是“逢山开路”了一下,可能还有更好的方法没有想到,或者一些细微处没有兼顾到,欢迎大家指出。如果大家有更好的,或者是因为本人对ASP.NET MVC认识不足而导致的“多此一举”也欢迎探讨!
下面介绍一下我的思路的一种做法:
我们要同时解决URL和Action方面的问题,那么首先当然是要考虑到,如何获取到请求页的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.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中调用,方法如下:
public void About()
{
// 以下Response.Write语句只做在V中确定这里曾经执行过逻辑的标记
System.Web.HttpContext.Current.Response.Write("我在Controller中输出了一串字符");
//关键是这句
if (!ActionOnlyForControllers.CheckAO(RouteData))
RenderView("About");
}
大家的说法我是同意的,但是这个语句并不是我在V里面输出数据的方法,这儿这么做只是为了能在页面执行后留下这段逻辑处理的“痕迹”。对应于我做的这个简单示例:ActionOnlyForControllers的一个简单示例 。 这里只是为了便于测试(因为只有这样不用牵扯到下游aspx文件),同样的效果大家可以把这句话理解为:
ViewData["somewords"]="我在Controller中输出了一串字符";
然后在aspx中调用 ViewData["somewords"]。
谢谢!
可以实现“留在页面”或者“转向页面”的判断。
在HTML中我们只需要用一个ao参数来控制:ao=0或为空,则不执行判断,保持MVC原有形式。ao为其他值时,则执行完Action仍然回到本页面,其效果类似简单的postback一下。我们可以这样做:
<%= 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引用到其他项目中,以免出错。
转载请注明出处和作者,谢谢!
作者:JeffreySu / QQ:498977166
博客:http://szw.cnblogs.com/
Senparc官方教程《微信开发深度解析:微信公众号、小程序高效开发秘籍》,耗时2年精心打造的微信开发权威教程,点击这里,购买正版!