前言
在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的功能,抽離出過濾的接口,來對於一些請求的過濾,那么功能上就可以被進一步的擴展了。
結尾
由於以往在寫文章的時候,都會提供詳細的實現源碼,但是后來發現這樣並不能給其他人自己實現的機會,因此這次就不提供源碼了,大部分重點的想法已經在文章中了,大家可以嘗試自己去實現,由於寫的文章也不多,如果有閱讀上的困難,請告訴我,我會發源碼給各位,文章中如有任何錯誤和遺漏請大家指出,謝謝大家。