通过IHttpHandlerFactory,AddParsedSubObject与配置文件来实现控件的动态加载.原本在看到此方案的时候觉的想到这方案的朋友特别高深,后来本人把本文写出来想和大家分享下,才知道这个就是Dottext方案中的一个模块而已.真是汗啊,嫌自己见识太浅。
最新更新:原本在看到此方案的时候觉的想到这方案的朋友特别高深,后来本人把本文写出来想和大家分享下,才知道这个就是Dottext方案中的一个模块而已.真是汗啊,嫌自己见识太浅。既然写了就当复习了一次Dottext.。这还是我第一次听说Dottext.
在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提供下载,希望对这方面感兴趣的朋友会有所帮助。此种方案并非最优,要看不同项目的具体需求了,关键的是能够解决问题.本文旨在和园友们互相讨论,如有不足的地方请指教. 程序下载