web form中自定义HttpHandler仿mvc
前言
在mvc大行其道的今天,仍然有不少公司的项目还是使用web form来实现的(其实mvc也是基于web form的),如果要在项目中引入mvc,不得不新建一个mvc的项目,然后将当前项目的功能一点点的转移过去,实在是很麻烦的一件事情,而且项目的改造周期也会加长,更别说一边改造一边添加新功能了,那么如果中间出现那么一点点的小差错,那么开发人员和测试人员估计想死的心都有了。
基于以上的情景,我们可以通过自定义HttpHandler来仿造mvc的模式,大概的实现思路如下:
- 给页面提供一个PageBase<TModel>的类来继承,中间类似于mvc中存放Model的容器
- 通过类似/mvc/controller/action方式的url对于Controller内Action的调用(之前《C#实现简易ajax调用后台方法》这篇文章有简单介绍过)
- 不同的action返回不同的ActionResult(如文本、Json等)
- 将自定义的MvcHandler在web.config中进行配置并引用相关的库即可
实现
首先我们需要自定义一个IHttpHandler来处理我们定义的mvc规则,并对其进行解析,其实原理就是上面提到的文章,只是Controller的Action会跟mvc的相似,返回ActionResult,代码大致如下:
public abstract class ActionResult
{
public abstract void ExecuteResult(HttpContext context);
}
public class MvcHandler : IHttpHandler, IRequiresSessionState
{
public const string PREFIX = "/mvc/";
//其他代码略
public void ProcessRequest(HttpContext context)
{
string path = context.Request.AppRelativeCurrentExecutionFilePath.Substring(PREFIX.Length);
Int32 index = path.LastIndexOf("/");
string route = path.Substring(0, index).ToLower();
string actionName = path.Substring(index + 1);
//反射获取Controller和Action
var controller = null;
var action = null;
var actionParamters = action.GetParameters();
object[] parameters = Array.ConvertAll(actionParamters, p =>
{
if (p.ParameterType == typeof(HttpPostedFile))
{
return context.Request.Files[p.Name];
}
return Convert.ChangeType(collection[key], type);
});
var result = action.Invoke(controller, parameters, null) as ActionResult;
if (result != null)
result.ExecuteResult(context);
}
然后在web.config内的HttpHandlers内添加<add path="/mvc/*/*" type="Infrastructure.MvcHandler" verb="POST,GET"/>,规则可以任意定制,但是得注意url的格式,如果定义成了*/*/*那么多拦截到全部的请求,那么难度就增加了。
接下来是页面,与以往aspx页面不同的是,我们需要在页面上调用到相应的Model,那么对于PageBase<TModel>就需要一个可以get Model的属性,代码如下:
public class DynamicPageBase : Page { public T Model { protected get; set; } }
但是由于我们在页面内调用Model之前,是要对其赋值的,因此就需要一个接口,代码改造如下:
public interface IMvcPage { void SetModel(object model); } public class DynamicPageBase : Page, IMvcPage { private T m_Model = default(T); protected T Model { get { return m_Model; } } public void SetModel(object model) { if (model != null) m_Model = (T)model; } }
在页面上,我们就可以使用<%=Model.XXX%>的方式来获取Model内的相关属性了,对于页面的改造大致已经完成了
那么我们怎么样像mvc那样通过/controller/action的方式来返回html呢,使用过mvc的朋友应该知道,我们的view是要放在一些特定的位置下的,如相应的Controller文件夹内包含着相应的Action aspx页面或razor页面
因此我们也可以在Web Form的目录下创建一个Views的文件夹,专门用来存放所有对应的Action页面,然后通过对url的解析来获取相应的页面,并将页面转化为html返回给客户端,ViewResult大致代码如下:
string html = ""; try { string childPath = context.Request.AppRelativeCurrentExecutionFilePath.Replace(MvcHandler.PREFIX, string.Empty); string virtualPath = string.Format("~/Views/{0}.aspx", childPath); IMvcPage page = PageParser.GetCompiledPageInstance(virtualPath, context.Server.MapPath(virtualPath), context) as IMvcPage; if (page != null) page.SetModel(m_model); using (StringWriter sw = new StringWriter()) { context.Server.Execute(page, sw, false); html = sw.ToString(); } } catch (Exception) { html = "无法访问该视图"; } context.Response.Write(html);
其他的ActionResult都是根据返回类型的不同而有不同的实现,我就不详细列举出来了。
扩展
相信留意过老赵博客的朋友都看过《技巧:使用User Control做HTML生成》、《方案改进:直接通过User Control生成HTML》这两篇关于UserControl的文章,那么我们可以参考里面的实现来对页面也添加相似的功能,并整合两种方案,让你的ViewResult可以生成aspx或ascx的html,我自己实现的规则是在页面不存在的情况下,则查找对应的UserControl是否存在,如果存在则返回UserControl的html,不存在的话则返回以上的无法访问视图的提示,代码改造大致如下:
//MvcHandler内 string pageVirtualPath = "页面虚拟路径"; string controlVirtualPath = "用户控件虚拟路径"; //aspx if (File.Exists(context.Server.MapPath(pageVirtualPath))) { var page = manager.LoadPage(pageVirtualPath) as IMvcPage; if (page != null) page.SetModel(m_model); html = manager.RenderView(); } //ascx else if (File.Exists(context.Server.MapPath(controlVirtualPath))) { var control = manager.LoadControl(controlVirtualPath); html = manager.RenderView(); } else { html = "无法访问该视图"; } public class ViewManager { //其他代码略 public Page LoadPage(string virtualPath) { m_page = PageParser.GetCompiledPageInstance(virtualPath, m_context.Server.MapPath(virtualPath), m_context) as Page; s_cache.SetViewPropertyValue(m_page, m_context.Request); return m_page; } public Control LoadControl(string virtualPath) { m_page = new Page(); m_control = m_page.LoadControl(virtualPath); m_page.Controls.Add(m_control); s_cache.SetViewPropertyValue(m_control, m_context.Request); return m_control; } }
对于MvcHandler而言,我们可以将部分的可变参数抽离出去,然后额外的进行实现,那么仿mvc的代码就可以整理到一个dll中,可以让其他的项目重用了。
然后就是可以在MvcHandler内再添加一些Filter的功能,抽离出过滤的接口,来对于一些请求的过滤,那么功能上就可以被进一步的扩展了。
结尾
由于以往在写文章的时候,都会提供详细的实现源码,但是后来发现这样并不能给其他人自己实现的机会,因此这次就不提供源码了,大部分重点的想法已经在文章中了,大家可以尝试自己去实现,由于写的文章也不多,如果有阅读上的困难,请告诉我,我会发源码给各位,文章中如有任何错误和遗漏请大家指出,谢谢大家。