专栏作品
如何构建积木式Web应用
刘志波
刘志波
上下文
基本上我们在儿童时代都玩过积木玩具。通过一块块的积木,再加上我们的想象力,就可以构造出非常多不同的风格的建筑。那么, 我们可不可以把这种搭积木的方式应用到我们的web应用上呢。
基本上我们在儿童时代都玩过积木玩具。通过一块块的积木,再加上我们的想象力,就可以构造出非常多不同的风格的建筑。那么, 我们可不可以把这种搭积木的方式应用到我们的web应用上呢。
问题
web应用通过提供给用户一整套组件(相当于积木),以及一套已经成型的方案(相当于图纸)。用户可以采用类似搭建积木的方式来根据自己的需要制作界面和应用。
web应用通过提供给用户一整套组件(相当于积木),以及一套已经成型的方案(相当于图纸)。用户可以采用类似搭建积木的方式来根据自己的需要制作界面和应用。
环境
采用asp.net 1.0或asp.net 1.1
采用asp.net 1.0或asp.net 1.1
预备知识
C#,asp.net的服务器控件编程,asp.net服务器控件生命周期
C#,asp.net的服务器控件编程,asp.net服务器控件生命周期
最好具备以下知识
Page Controller,Front Controller
Page Controller,Front Controller
解决方案
1.建立目录结构
为了了解如何采用积木块式应用,我们首先建立如下的主题目录结构:
1.建立目录结构
为了了解如何采用积木块式应用,我们首先建立如下的主题目录结构:
PageTeamplate.ascx是页面布局框架,根据所请求的页面不同,在PageTemplate的主体位置载入不同Page目录下的ascx文件
2.划分自己的web积木——UserControl
将web应用分成一块块的积木,每一块积木形成一个UserControl,并且每一个UserControl有一个同名的css文件用来控制界面。最基本的积木块要放在UserControl目录下,而由最基本积木块组成的页面文件放在Page目录下。
提示:在一般的web应用中,都会有Header,Footer,Login等等这样的模块,这些模块就可以形成UserControl组成web应用的积木。具体积木块应该根据你的web应用功能来划分,一般来说我们可以把某个功能就划分成一个积木。
3.构建载入积木块的容器——MyPlaceHolder
有了UserControl这些积木块之后,就需要有能够自动在应用中载入这些UserControl积木的容器,这就是PlaceHolder。不过,我们需要扩展PlaceHolder的功能达到自动载入UserControl的目的。
public class MyPlaceHolder : PlaceHolder { private string userControl; // 要载入的UserControl目录下的.ascx private string pageControl; // 要载入的Page目录下的.ascx public MyPlaceHolder() { userControl = ""; pageControl = ""; } public string UserControl { get { return userControl; } set { userControl = value; } } public string PageControl { get { return pageControl; } set { pageControl = value; } } // 当需要载入多个UserControl时,可以直接调用LoadUserControl // 当只需要载入一个UserControl时,可以调用Clear清除载入过的内容 public void Clear() { this.Controls.Clear(); } // 载入UserControl目录下的.ascx // 以及导入对应的css文件 public void LoadUserControl(string UserControl) { this.userControl = UserControl; BasePage page = (BasePage)this.Page; // 请参考后面的BasePage的代码 Control control = this.Page.LoadControl( page.Scheme + "usercontrol/" + userControl + ".ascx"); string css = "css/" + userControl + ".css"; // 对应的css文件 if(File.Exists(this.Page.MapPath(page.Scheme+css))) { page.AddCss(page.Scheme + css); } this.Controls.Add(control); } // 载入Page目录下的.ascx // LoadPage与LoadUserControl的区别是两者载入的.ascx所在的目录不同 // Page目录下的.ascx可以看成是一些搭建主体结构的.ascx,其使用MyPlaceHolder // 来包含最基础的积木块.ascx(在UserControl目录下) public void LoadPage(string PageControl) { this.PageControl = PageControl; BasePage page = (BasePage)this.Page; Control control = this.Page.LoadControl( page.Scheme + "page/" + pageControl + ".ascx"); string css = "css/" + pageControl + ".css"; if(File.Exists(this.Page.MapPath(page.Scheme+css))) { page.AddCss(page.Scheme + css); } this.Controls.Add(control); } protected override void OnLoad(EventArgs e) { base.OnLoad(e); if(!userControl.Equals(string.Empty)) { LoadUserControl(userControl); } } } public class MyPlaceHolder : PlaceHolder { private string userControl; // 要载入的UserControl目录下的.ascx private string pageControl; // 要载入的Page目录下的.ascx public MyPlaceHolder() { userControl = ""; pageControl = ""; } public string UserControl { get { return userControl; } set { userControl = value; } } public string PageControl { get { return pageControl; } set { pageControl = value; } } // 当需要载入多个UserControl时,可以直接调用LoadUserControl // 当只需要载入一个UserControl时,可以调用Clear清除载入过的内容 public void Clear() { this.Controls.Clear(); } // 载入UserControl目录下的.ascx // 以及导入对应的css文件 public void LoadUserControl(string UserControl) { this.userControl = UserControl; BasePage page = (BasePage)this.Page; // 请参考后面的BasePage的代码 Control control = this.Page.LoadControl( page.Scheme + "usercontrol/" + userControl + ".ascx"); string css = "css/" + userControl + ".css"; // 对应的css文件 if(File.Exists(this.Page.MapPath(page.Scheme+css))) { page.AddCss(page.Scheme + css); } this.Controls.Add(control); } // 载入Page目录下的.ascx // LoadPage与LoadUserControl的区别是两者载入的.ascx所在的目录不同 // Page目录下的.ascx可以看成是一些搭建主体结构的.ascx,其使用MyPlaceHolder // 来包含最基础的积木块.ascx(在UserControl目录下) public void LoadPage(string PageControl) { this.PageControl = PageControl; BasePage page = (BasePage)this.Page; Control control = this.Page.LoadControl( page.Scheme + "page/" + pageControl + ".ascx"); string css = "css/" + pageControl + ".css"; if(File.Exists(this.Page.MapPath(page.Scheme+css))) { page.AddCss(page.Scheme + css); } this.Controls.Add(control); } protected override void OnLoad(EventArgs e) { base.OnLoad(e); if(!userControl.Equals(string.Empty)) { LoadUserControl(userControl); } } }
使用方法: <HomeOffice:MyPlaceHolder id="Myplaceholder1" runat="server" UserControl="Header"> </HomeOffice:MyPlaceHolder> // 这里的Header是位于UserControl目录下的Header.ascx
<%@ Register TagPrefix="HomeOffice" Namespace="HomeOffice.Web.UI.WebControl" Assembly = "HomeOffice.Web.UI" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <HTML> <HEAD> <title>构建积木式应用程序</title> <asp:Literal ID="CssHolder" runat="server"></asp:Literal> <asp:Literal ID="ScriptHolder" Runat="server"></asp:Literal> <style> BODY { margin-left : 0px; margin-right : 0px; } </style> </HEAD> <body bgcolor="#e6e6e6"> <form id="Form1" method="post" runat="server" enctype="multipart/form-data"> <table width="100%" cellpadding="0" cellspacing="0"> <tr> <td> </td> <td width="800"> <table width="100%" cellpadding="0" cellspacing="0"> <tr> <td> <HomeOffice:MyPlaceHolder id="PlaceHolder1" runat="server" UserControl="Header"> </HomeOffice:MyPlaceHolder> </td> </tr> <tr> <td> <HomeOffice:MyPlaceHolder id="Myplaceholder1" runat="server" UserControl="MainMenu"> </HomeOffice:MyPlaceHolder> </td> </tr> <tr> <td style="height:6px;background:#f6f6f6;font-size:1px; border-top:1px solid white;"> </td> </tr> <tr> <td style="height:4px;background:#e1e1e1;font-size:1px; border-top:1px solid #e6e6e6; "> </td> </tr> <tr> <td style="background:white;border-bottom:1px solid #bbbbbb"> <HomeOffice:MyPlaceHolder id="PageBody" runat="server"> </HomeOffice:MyPlaceHolder> </td> </tr> <tr> <td style="padding-top:20px"> <HomeOffice:MyPlaceHolder id="Myplaceholder2" runat="server" UserControl="Footer"> </HomeOffice:MyPlaceHolder> </td> </tr> </table> </td> <td> </td> </tr> </table> </form> </body> </HTML>
4.构建主要的建筑结构——PageTemplate.ascx
PageTemplate其实也是一个UserControl,只不过其功能是用来包含其他的UserControl积木,在PageTemplate里,可以定义页面的整体布局。比如:Header、Footer在整个页面中的位置,页面主体区域的位置等等。
更重要的是,PageTemplate中应该包含Form的定义,这是asp.net所需要的不可缺少的服务器控件。
public class BasePage : Page { public string Scheme = "/Scheme/blue/"; // 所采用的主题 public AppSetting Setting; // 环境配置,在Init中分析,其内容包括解析http请求到正确的Page目录下的 // 文件,建立当前登陆用户的信息 public Control focusControl; // 当页面载入后,首先获得焦点的控件 private Literal CssHolder; // 要导入的css private Literal ScriptHolder; // 要导入的script文件 public BasePage() { focusControl = null; } // 导入css文件引用 public void AddScript(string script) { // 进行IsPostBack判断的原因是 // 防止重复导入 if(!this.IsPostBack) { ScriptHolder.Text += string.Format("<script src=\"{0}\" type=\"text/javascript\"></script>\n", script); } } // 导入script文件引用 public void AddCss(string css) { if(!this.IsPostBack) { CssHolder.Text += "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + css + "\">\n"; } } // 载入http请求分析后的Page目录下的所请求的文件 public void LoadPageTemplate() { Control control = (Control)this.LoadControl (this.Scheme+"PageTemplate.ascx"); CssHolder = (Literal)control.FindControl("CssHolder"); ScriptHolder = (Literal)control.FindControl("ScriptHolder"); this.Controls.Add(control); MyPlaceHolder body = (MyPlaceHolder) control.FindControl("PageBody"); body.LoadPage(this.Setting.TargetPage); // 调用MyPlaceHolder的LoadPage方法 // TargetPage记录了请求的页面 } protected override void OnInit(EventArgs e) { base.OnInit(e); // 分析http请求 Setting = new AppSetting(this.Request.Path); // 设置用户信息 if(this.Request.IsAuthenticated) { Setting.SetUser(User.Identity.Name); } } protected override void OnLoad(EventArgs e) { base.OnLoad(e); this.LoadPageTemplate(); } // 当页面显示后,初始获得焦点的控件 protected void SetFocusControl() { if(this.focusControl==null) return; string template = @"<script language='jscript'> document.all.{0}.focus();</script>"; string script = string.Format(template, this.focusControl.ClientID); this.RegisterStartupScript("FocusControl", script); } protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); SetFocusControl(); } // 修复了asp.net 1.1的一个bug // 没有这段代码,LinkButton等某些服务器将无法使用 protected override void Render(HtmlTextWriter writer) { StringBuilder stringBuilder = new StringBuilder(); StringWriter stringWriter = new StringWriter(stringBuilder); HtmlTextWriter htmlWriter = new HtmlTextWriter(stringWriter); base.Render(htmlWriter); string html = stringBuilder.ToString(); int start = html.IndexOf("<form name=\"") + 12; int end = html.IndexOf("\"", start); string formID = html.Substring(start, end - start); string replace = formID.Replace(":", "_"); html = html.Replace("document."+formID,"document."+replace); writer.Write(html); } }
BasePage中的一个非常重要的成员变量Setting,其作用是保存分析http请求后的结果。由于采用积木式应用,用户所请求的aspx文件在硬盘上并不存在,需要把用户的这种http请求解析成Page目录下的某个ascx文件,让BasePage载入。
5. 采用HttpHandler截获http请求
有了这些基础性的东西之后,我们就应该使用Front Contrller模式来控制所请求的aspx文件,再把这些请求的文件导向正确的UserControl积木。
.NET framework中的Page继承了IhttpHandler,因此我们可以对aspx的解析就让BasePage来处理,这样BasePage在分析了http请求之后,会载入PageTemplate,然后再根据所请求的页面载入不同的UserControl积木。
在web.config中进行如下配置,让httphandler生效: <httpHandlers> <add verb="*" path="*.aspx" type="HomeOffice.Web.UI.HttpHandler.MyPageHandlerFactory, HomeOffice.Web.UI"/> </httpHandlers>
我们在这里不是直接使用了HttpHandler,而是采用了HttpHandlerFactory,不过所使用的代码也非常简单,用户自己可以扩展它的功能。
public class MyPageHandlerFactory : IHttpHandlerFactory { public virtual IHttpHandler GetHandler(HttpContext context, String requestType, String url, String pathTranslated) { return new BasePage(); } public virtual void ReleaseHandler(IHttpHandler handler) { } }
public class AppSetting { public string Url; // request url public string Site; // request site public string User; // login name public string UserName; // display name public string TargetPage; // target page public string[] Roles; // user roles in the site private Hashtable parameter = new Hashtable(); public AppSetting() { Site = "default"; User = "*"; UserName = ""; Roles = null; } public AppSetting(string url) : this() { this.Url = url.ToLower(); AnalysisUrl(this.Url); } public object this[string key] { get { return parameter[key]; } set { parameter.Add(key, value); } } // 分析用户所请求的页面和参数 protected void AnalysisPage(string url) { PageInfo[] pages = XmlHomeOffice.Pages(); foreach(PageInfo page in pages) { if(Regex.IsMatch(url,page.pattern,RegexOptions.IgnoreCase)) { this.TargetPage = string.Format(page.target_page, url.Replace(".aspx","").Split('/')); if(!page.parameter.Equals(string.Empty)) { string p = string.Format(page.parameter, url.Replace(".aspx", "").Split('/')); string[] ps = p.Split(','); foreach(string str in ps) { string[] item = str.Split('='); this[item[0]] = item[1]; } } return; } } // No one matched, a Exception occur this.TargetPage = "error"; } // 分析出站点,类似于blog中的每个站点 protected string AnalysisSite(string url) { if(url[0]=='/') { url = url.Remove(0, 1); } string[] items = url.Split('/'); if(items.Length<1) { return Site + "/default.aspx"; } if(items[0].EndsWith(".aspx")) { return Site + "/" + url; } string[] reserved_words = XmlHomeOffice.ReservedWords(); foreach(string str in reserved_words) { if(items[0].Equals(str.ToLower())) { return Site + "/" + url; } } Site = items[0]; return url; } public void AnalysisUrl(string url) { Url = AnalysisSite(url); AnalysisPage(Url); } public void SetUser(string user) { User = user; UserName = XmlUsers.GetDisplayName(user); XmlSiteProfile profile = new XmlSiteProfile(Site); Roles = profile.SiteUserRole(user); } public bool HasRole(string role) { if(Roles==null) return false; foreach(string str in Roles) { if(str.Equals(role)) return true; } return false; } }
<pages> <page pattern="^(\/testboth.aspx)$" parameter="" target-page="testboth" /> <page pattern="^(\/\w+.aspx)$" parameter="" target-page="{1}" /> </pages>
由于采用了HttpHandler来映射新的页面,因此象HyperLink,Image等这些与相对路径有关的服务器控件就需要做个小手术来适应这种改变。
7.如何开发其他主题
要进行其他主题积木的开发,只需要开发界面工作即可。后台代码文件不需要开发。
Aspx文件前的指示语句为:
<%@ Control Language="c#" AutoEventWireup="false" Inherits="HomeOffice.UserControl.PageTemplate" %>
流程说明
以某个请求为例,对积木式web应用的流程进行说明假设用户请求localhost/default.aspx文件,在xml文件中分析得到default.aspx文件所对
应的处理ascx为Page目录下的HomePage.ascx。
首先由BasePage载入PageTemplate.ascx,然后根据分析出来的TargetPage载入
HomePage。
具体应用
采用这种方式构建的web应用即将在www.smartyouth.net开通。其主要功能有:积木式应用,风格多样化(采用类似于windows主题的方式)。
优点讨论
下载
在采用积木式构造www.smartyouth.net过程中,发现了几个小的优点:1.可以统一的对应用中的权限进行认证。
实现一个权限认证UserControl,在该UserControl中判断用户是否具有某权限,是否符合某种角色,然后将认证的结果保存在BasePage中的IsAuthorized字段中,当认证不通过时显示某些错误字符串。
对于任何需要权限认证的模块,将该UserControl直接拖进设计器,并且正确的设置权限属性和角色属性即可。接着在需要认证的模块中将某些Button的Enabled状态设置为BasePage.IsAuthorized即可。
2.统一的Validator
由于asp.net的Validator模型有时候不是非常的方便,采用积木式应用,也可以统一的解决Validator。
实现一个Validator UserControl,在BasePage中用一个对象来记录错误字符串和对应的Control。
在Validator UserControl的PreRender方法中,将记录了错误的对象中所有的内容生成一些Link(当单击该Link,对应的Control调用focus方法)。
在需要进行检查的UserControl中,如果发生了错误,将错误字符串和Control.ClientID添加到错误对象中即可。
建立如下的数据库,表,存储过程: 数据库:test 访问账号:sa,sa(可以在web.config中修改) 表:test 字段 test_id(自增),string(nvarchar(50)),number(int) 存储过程: testaddnew:(没有参数) insert into test(string, number) values(‘’, 0); testget: (没有参数) select * from test testdelete:(输入参数:@test_id int) delete from test where test_id = @teat_id testupate:(参数:@test_id, @string, @number) update test set string=@string, number=@number where test_id=@test_id
http://localhost/testtextbox.aspx
http://localhost/testdatagrid.aspx
http://www.smartyouth.net/scheme.rar
相关知识
http://www.microsoft.com/china/msdn/architecture/patterns/Esp/http://scottwater.com/DotText/default.aspx