在BS架構這塊,我們平時建一個網站,通常是創建一個WebSite網站,或者是創建一個WebApplication.然後在WEB項目裡面建立相應的aspx文件,以及用戶控件,自定義控件等等.但是做為一個新加入此項目的你要去修改裡面的
某一個功能模板,那麼我一般的程序是這樣的:
第一:根據新的需求,在項目找到相關頁面,例如對應的頁面URL為index.aspx.此時可以對原頁面做最初的了解.
第二:分析代碼.
第三:根據新的需求來修改代碼.
我想這也是一般朋友的思維模式吧.可是這樣的流程並不是統一不變的,至於如何變化,我先想和園友們討論一下如下的需求大家是如何實現的?
網站可能有多個版本,例如最起碼的中英日版,各版本的內容呈現風格上也略有不同.
本人列舉三個本人想到的做法:
第一種方法:每個版本單獨建一個站點.具體要求具體實現.這種做法的缺點是維護困難,成本高.
第二種方法:建一個站點,為每個版本的同一個功能頁面分別創建單獨的頁面.例如一個顯示新聞的頁面,分別為:中文新聞.aspx,英文新聞.aspx等.這種方法的缺點同上,只不過稍微會容易點.
第三種方法:建一個站點,一個功能就建一個頁面,但為不同版本創建不同的用戶控件,根據相應的參數為動態加載控件.這種做法是最優的.(個人認為)這做做法的缺點是程序的耦合度太強,如果是要增加一個版本,那麼就要修改功能頁面.
其實上面的第三種方法一般來說是可以解決問題的.後來在新的項目中發現了httpHandler的應用,可以自定義的捕捉到http請求.發現了一個全新的解決方案.
IHttpHandlerFactory在.net中算是用處比較多的。它能夠在Hander對象生成前對當前頁面的Hander進行預處理。一般我們如果是做URL重寫的話,也會用到httphandler,現在可以利用它來實現我上面的需求.
網站呢,就建一個頁面webform2.aspx.在這個頁面中根據請求的URL來動態加載用戶控件.而這個動態加載程序並不是寫在webform2中的pageload事件中.而是寫的頁面的AddParsedSubObject事件中.當用戶訪問別外一個頁面,例如一個搜索頁search.aspx,此時站中並不實際存在這個頁面.而是利用httphandler來捕獲這個HTTP請求,定向於已經實際存在的webform2.aspx頁面,在webform2.aspx頁面中的AddParsedSubObject事件中完成搜索控件的加載.
第一:如何來自定義的來捕獲HTTP請求呢?
1:建立一個實現了接口IHttpHandlerFactory的類.
IHttpHandlerFactory 類型公開了以下成員。
方法 :
GetHandler 返回實現 IHttpHandler 接口的類的實例。
ReleaseHandler 使工廠可以重用現有的處理程序實例。
Code
public class WebPageHandlerFactory : IHttpHandlerFactory
{
public virtual IHttpHandler GetHandler(HttpContext context, string requestType, string url, string path)
{
if (IsExistedPage(path))
{
return PageParser.GetCompiledPageInstance(url, path, context);
}
if (!path.ToLower().EndsWith(".aspx"))
{
path = Path.Combine(path, "webform2.aspx");
}
url = ReplaceUrl(url);
path = ReplaceUrl(path);
return PageParser.GetCompiledPageInstance(url, path, context);
}
/**//// <summary>
/// 判斷當前請求的URL是否真實存在
/// </summary>
/// <param name="path">請求URL的相對路徑</param>
/// <returns></returns>
private bool IsExistedPage(string path)
{
//首頁是已經存在的
if (path.ToLower().IndexOf("webform2.aspx") > 0)
{
return true;
}
return false;
}
/**//// <summary>
/// 把實際不存在的頁面名稱轉換成已經真實存在的default.aspx
/// 例如:default2.aspx實際不存在,則把default2.aspx轉換成default.aspx
/// </summary>
/// <param name="input">當前請求的但實際並不存在的URL 例如:default2.aspx</param>
/// <returns></returns>
private string ReplaceUrl(string input)
{
// Regex search and replace
RegexOptions options = RegexOptions.None;
Regex regex = new Regex(@"w+.aspx", options);
string replacement = @"webform2.aspx";
string result = regex.Replace(input, replacement);
return result;
}
public virtual void ReleaseHandler(IHttpHandler handler)
{
}
}
2:配置WEB.CONFIG
<httpHandlers>
<add verb="*" path="/*.aspx" type="WebApplication_Test.WebPageHandlerFactory, WebApplication_Test"/>
</httpHandlers>
參數簡單說明:
path:要捕獲的文件路徑.
type:WebApplication_Test.WebPageHandlerFactory :實現了接口IHttpHandlerFactory的類
WebApplication_Test:WEB站的命名空間
這樣就可以捕獲HTTP請求了,例如請求index2.aspx,站點中並不存在,但是當你訪問時程序並不會報錯,而是會實際轉向到webform2.aspx.此時用戶看到的還是index2.aspx.
第二:如何實現根據URL來動態加載用戶控件呢?並不是在頁面用if條件判斷.這裡我們要提一下HtmlContainerControl.
HtmlContainerControl:
用作映射到需要具有開始標記和結束標記的 HTML 元素的 HTML 服務器控件的抽象基類。
我們通常在一個頁面動態加載控件,都是在頁面中放一個pannel,此pannel一般放在form中,這樣用戶控件中的服務器控件就可以正常運行了.然後把要加載的控件加入到pannel中即可.為此我們可以自定義一個繼承HtmlContainerControl的服務器控件來充當pannel容器.
總的實現過程如下:
1:建立的容器頁:WebForm2.aspx,站點中只存在這一個頁面,其它的頁面請求都是通過它運行的.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm2.aspx.cs" Inherits="WebApplication_Test.WebForm2" %>
<%@ Register TagPrefix="ccWebUI" Namespace="Common.UI" Assembly="Common.UI" %>
<ccWebUI:masterpagecontrol id="MPContainer" runat="server"></ccWebUI:masterpagecontrol>
頁面cs代碼
//裝入模板
Code
public partial class WebForm2 : BasePage
{
protected void Page_Load(object sender, EventArgs e)
{
}
//裝入模板
protected override void AddParsedSubObject(object obj)
{
AppSettingPage config = new AppSettingPage(Context);
base.AddParsedSubObject(obj);
if (obj is MasterPageControl)
{
string TemplateFilePath = config.TeamplateFilePath;
if (!string.IsNullOrEmpty(TemplateFilePath))
{
((MasterPageControl)obj).Controls.Clear();
((MasterPageControl)obj).TemplateFile = config.TeamplateFilePath;
}
}
}
}
AddParsedSubObject:
通知服務器控件某個元素(XML 或 HTML)已經過語法分析,並將該元素添加到服務器控件的 ControlCollection 對象。通過這個事件可以對頁面的所有控件進行分析並能夠進一步操作頁面加載過程,例如本文提出的根據URL來動態加載用戶控件.
AppSettingPage:頁面配置文件,它實現了根據用戶請求的URL來指定應該加載哪一個控件.
Code
public class AppSettingPage
{
//頁面訪問的URL
public string Url;
//模板頁面加載控件的路徑
public string TeamplateFilePath;
public AppSettingPage(string url)
{
this.Url = url.ToLower();
AnalysisUrl(this.Url);
}
public AppSettingPage(HttpContext context)
{
string url = context.Request.Url.ToString();
this.Url = url.ToLower();
AnalysisUrl(this.Url);
}
/**//// <summary>
/// 根據當前請求的URL來取相應控件的路徑
/// </summary>
/// <param name="url">當前請求的URL</param>
public void AnalysisUrl(string url)
{
//讀取模板信息,取出所有控件路徑列表
PageInfo[] pages = TemplateConfig.Pages();
foreach (PageInfo page in pages)
{
if (Regex.IsMatch(url, page.pattern, RegexOptions.IgnoreCase))
{
//如果當前請求的URL在配置文件中則取相應控件的路徑
this.TeamplateFilePath = page.target_page;
break;
}
}
}
}
類:TemplateConfig.cs 它根據URL來與TemplateConfig.xml相匹配,返回要加載控件的路徑,這樣做就可以實現加載控件與頁面邏輯的解耦了,只要在配置文件中配置好URL與控件路徑的對應關系就行。
Code
/**//// <summary>
/// XmlScheme 的摘要說明。
/// </summary>
public struct PageInfo
{
//頁面名稱
public string pattern;
//控件路徑
public string target_page;
}
[System.Serializable]
public class TemplateConfig
{
public TemplateConfig() { }
public static XmlDocument doc = null;
public static PageInfo[] Pages()
{
doc = new XmlDocument();
//加載模板文檔
doc.Load(HttpContext.Current.Request.MapPath("TemplateConfig.xml"));
if (doc == null)
{
return null;
}
DataTable _dr = XmlDataTable(doc, "pages/page");
if (_dr.Rows .Count <1) return null;
//模板個數
int iTemplateCount = _dr.Rows .Count;
PageInfo[] info = new PageInfo[iTemplateCount];
for (int i = 0; i < iTemplateCount; i++)
{
info[i].pattern = _dr.Rows[i]["pageName"].ToString();
info[i].target_page = _dr.Rows[i]["target-page"].ToString ();
}
doc = null;
return info;
}
/**//// <summary>
/// 構造一個xml源的數據集
/// </summary>
/// <param name="strXpath"></param>
/// <param name="strKey"></param>
/// <param name="strContent"></param>
/// <returns></returns>
public static DataTable XmlDataTable(XmlDocument objXmlDoc, string strXpath)
{
XmlNodeList xmllist = objXmlDoc.SelectNodes(strXpath);
DataTable dt = new DataTable();
dt.Columns.Add("pageName");
dt.Columns.Add("target-page");
int iRows = xmllist.Count;
for (int i = 0; i < xmllist.Count; i++)
{
dt.Rows.Add(xmllist[i].Attributes["pageName"].Value, xmllist[i].Attributes["target-page"].Value);
}
return dt;
}
}
頁面配置文件TemplateConfig.xml:一個URL對應一個用戶控件。用戶可以把用戶實際訪問的頁面URL與相對應的用戶控件對應起來,這樣在httphander處理的時候就能夠找到正確的信息加載了.他的好處是修改功能不用修改程序,只要配置就可以.例如此時有另一個頁面中出現了一個變種頁面,就是增加了一個功能,此時只要寫好了相應的用戶控件,然後在此配置文件中加入配置信息<page pageName="新的頁面" target-page="新的用戶控件路徑"></page >,並不需要修改程序.
Code
<?xml version="1.0" encoding="utf-8" ?>
<pages>
<page pageName="index.aspx" target-page="index.ascx"></page >
<page pageName="index2.aspx" target-page="index2.ascx"></page >
</pages>
基類:BasePage.上面的webform2中並沒有出現傳統的html標記,一個頁面要想正常顯示相應的HTML標記是必不可少的.
Code
public class BasePage:Page
{
/**//// <summary>
/// 頁面標題
/// </summary>
public string pageTitle
{ get; set; }
/**//// <summary>
/// 重寫頁面的頭部HTML
/// </summary>
/// <param name="writer"></param>
protected virtual void RenderHeader(HtmlTextWriter writer)
{
StringBuilder strHeader = new StringBuilder();
strHeader.Append(
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">n" +
"<html xmlns="http://www.w3.org/1999/xhtml" lang="zh-CN">n" +
"<head>n");
strHeader.Append("<title>");
strHeader.Append(this .pageTitle );
strHeader.Append("</title>n");
strHeader.Append("</head><body>");
writer.Write(strHeader.ToString());
}
/**//// <summary>
/// 重寫頁面的尾部HTML
/// </summary>
/// <param name="writer"></param>
protected virtual void RenderFooter(HtmlTextWriter writer)
{
StringBuilder strHeader = new StringBuilder();
string strFoot = "</body></html>";
writer.Write(strFoot);
}
/**//// <summary>
/// 重寫頁面
/// </summary>
/// <param name="writer"></param>
protected override void Render(HtmlTextWriter writer)
{
RenderHeader(writer);
base.Render(writer);
RenderFooter(writer);
}
}
masterpagecontrol:一個自定義容器的服務器控件:
Code
//此控件為一個容器,負責加載不同的用戶控件(根據請求的URL)
[ToolboxData("<{0}: MasterPageControl runat=server></{0}: MasterPageControl>"),
ToolboxItem(typeof(WebControlToolboxItem)),
Designer(typeof(ReadWriteControlDesigner))]
[System.Serializable]
public class MasterPageControl : HtmlContainerControl
{
private string templateFile="";
private string defaultContent;
private Control template = null;
/**//// <summary>
/// 模板控件中將來要加載的控件路徑
/// </summary>
[Category("MasterPage"), Description("Path of Template User Control")]
public string TemplateFile
{
get
{
return this.templateFile ;
}
set { this.templateFile = value; }
}
public MasterPageControl()
{
}
/**//// <summary>
/// 模板容器控件初始化
/// </summary>
/// <param name="e"></param>
protected override void OnInit(EventArgs e)
{
this.BuildMasterPage();
base.OnInit(e);
}
/**//// <summary>
/// 加載用戶控件
/// </summary>
private void BuildMasterPage()
{
if (!string.IsNullOrEmpty(TemplateFile))
{
this.template = this.Page.LoadControl(TemplateFile);
this.template.ID = this.ID + "_Template";
this.Controls.AddAt(0, this.template);
return;
}
}
protected override void RenderBeginTag(HtmlTextWriter writer) { }
protected override void RenderEndTag(HtmlTextWriter writer) { }
}
為此就以本文前面的需求來說,例如有一個新聞頁面,他有三個版本.我們只需求建好三個用戶控件,分別實現讀取新聞功能.配置文件如下,此後用戶就可以通過訪問中文新聞.aspx來訪問中文版的了,其它的同理.如果此時多了一個韓文版的,只要建好一個韓文新聞.ascx,加上配置文件就行.
Code
<?xml version="1.0" encoding="utf-8" ?>
<pages>
<page pageName="中文新聞.aspx" target-page="中文新聞控件.ascx"></page >
<page pageName="英文新聞.aspx" target-page="英文新聞控件.ascx"></page >
<page pageName="日文新聞.aspx" target-page="日文新聞控件.ascx"></page >
</pages>
本人寫的不太合理,可能大部分朋友有可能看不懂。為此特將本人的DEMO提供下載,希望對這方面感興趣的朋友會有所幫助。此種方案並非最優,要看不同項目的具體需求了,關鍵的是能夠解決問題.本文旨在和園友們互相討論,如有不足的地方請指教.
本文配套源碼