天天看點

web form中自定義HttpHandler仿mvc

前言

  在mvc大行其道的今天,仍然有不少公司的項目還是使用web form來實作的(其實mvc也是基于web form的),如果要在項目中引入mvc,不得不建立一個mvc的項目,然後将目前項目的功能一點點的轉移過去,實在是很麻煩的一件事情,而且項目的改造周期也會加長,更别說一邊改造一邊添加新功能了,那麼如果中間出現那麼一點點的小差錯,那麼開發人員和測試人員估計想死的心都有了。

  基于以上的情景,我們可以通過自定義HttpHandler來仿造mvc的模式,大概的實作思路如下:

  1. 給頁面提供一個PageBase<TModel>的類來繼承,中間類似于mvc中存放Model的容器
  2. 通過類似/mvc/controller/action方式的url對于Controller内Action的調用(之前《C#實作簡易ajax調用背景方法》這篇文章有簡單介紹過)
  3. 不同的action傳回不同的ActionResult(如文本、Json等)
  4. 将自定義的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的功能,抽離出過濾的接口,來對于一些請求的過濾,那麼功能上就可以被進一步的擴充了。

結尾

  由于以往在寫文章的時候,都會提供詳細的實作源碼,但是後來發現這樣并不能給其他人自己實作的機會,是以這次就不提供源碼了,大部分重點的想法已經在文章中了,大家可以嘗試自己去實作,由于寫的文章也不多,如果有閱讀上的困難,請告訴我,我會發源碼給各位,文章中如有任何錯誤和遺漏請大家指出,謝謝大家。