如何构建积木式Web应用 
如何构建积木式Web应用
刘志波 
--------------------------------------------------------------------------------上下文基本上我们在儿童时代都玩过积木玩具。通过一块块的积木,再加上我们的想象力,就可以构造出非常多不同的风格的建筑。那么, 我们可不可以把这种搭积木的方式应用到我们的web应用上呢。问题web应用通过提供给用户一整套组件(相当于积木),以及一套已经成型的方案(相当于图纸)。用户可以采用类似搭建积木的方式来根据自己的需要制作界面和应用。
环境
采用asp.net 1.0或asp.net 1.1
  
预备知识
C#,asp.net的服务器控件编程,asp.net服务器控件生命周期 
最好具备以下知识
Page Controller,Front Controller 
解决方案1.建立目录结构为了了解如何采用积木块式应用,我们首先建立如下的主题目录结构: 
在Sheme目录下保存所有的主题,每个主题都采用相同的目录结构。UserControl目录保存最基本的积木块(UserControl),Page目录是积木块组成的页面所形成的UserControl,Css目录保存与积木块同名的css文件来控制UserControl的界面。
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目录下的.ascxpublic 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目录下的.ascxpublic 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

解决方案 »

  1.   

    4.构建主要的建筑结构——PageTemplate.ascxPageTemplate其实也是一个UserControl,只不过其功能是用来包含其他的UserControl积木,在PageTemplate里,可以定义页面的整体布局。比如:Header、Footer在整个页面中的位置,页面主体区域的位置等等。
    更重要的是,PageTemplate中应该包含Form的定义,这是asp.net所需要的不可缺少的服务器控件。 
    <%@ 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>&nbsp;</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;">&nbsp;
    </td>
     </tr>
     <tr>
       <td style="height:4px;background:#e1e1e1;font-size:1px;
    border-top:1px solid #e6e6e6; ">&nbsp;
    </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>&nbsp;</td>
    </tr>
    </table>
    </form>
    </body>
    </HTML>在这个PageTemplate中,我们可以看到使用了4个MyPlaceHolder来载入积木块,除了PageBody这个外,其他的都载入了位于UserControl目录下最基本的积木块。而PageBody的作用则是根据http所请求的页面不同载入Page目录下不同的页面。这些其对应的cs代码在BasePage中处理。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载入。
      

  2.   

    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)
    {
    }
    }
    为了要正确载入用户所请求的页面,需要对其http请求分析,这个功能放在AppSetting中实现,AppSetting还有一个重要功能就是将http请求字符串转化为页面中将要使用的参数。比如请求http://localhost/news/1.aspx,将分析出目标页面为newsdetail.ascx,参数为new_id=1: 
    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;
     }}在分析http请求的页面时,要用到外部Xml文件记录的pages映射信息: 
      <pages>
     <page pattern="^(\/testboth.aspx)$" 
    parameter="" target-page="testboth" />
     <page pattern="^(\/\w+.aspx)$" 
    parameter="" target-page="{1}" />
      </pages>6.需要修改的服务器控件由于采用了HttpHandler来映射新的页面,因此象HyperLink,Image等这些与相对路径有关的服务器控件就需要做个小手术来适应这种改变。
    7.如何开发其他主题要进行其他主题积木的开发,只需要开发界面工作即可。后台代码文件不需要开发。
    Aspx文件前的指示语句为: 
    <%@ Control Language="c#" AutoEventWireup="false" 
    Inherits="HomeOffice.UserControl.PageTemplate" %>
    在这个指示语句里,不需要codehind属性。其实现代码已经实现在blue主题中。最好的方式是,将blue主题中的所有ascx的cs文件都独立到一个UserControlCs目录中。这样在所有ascx文件中Inherits=”HomeOffice.UserControlCs.PateTemplate”。 
    流程说明
    以某个请求为例,对积木式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添加到错误对象中即可。  下载
    注意:下载的源代码与示例代码会存在少少出入,因为示例代码来自一个更复杂些的应用。如果需要使用下载代码中的DataGrid示例,需要安装SQL Server,并且建立如下的数据库,表,存储过程:
    数据库:test
    访问账号:sa,sa(可以在web.config中修改)
    表:test
    字段 test_id(自增),string(nvarchar(50)),number(int)
    存储过程:
    testaddnew:(没有参数)
    insert into test(string, number) values(‘’, 0);testget: (没有参数)
    select * from testtestdelete:(输入参数:@test_id int)
    delete from test where test_id = @teat_idtestupate:(参数:@test_id, @string, @number)
    update test set string=@string, number=@number 
    where test_id=@test_id
    采用如下方式访问示例:(保证是localhost)
    http://localhost/testboth.aspx
    http://localhost/testtextbox.aspx
    http://localhost/testdatagrid.aspx下载地址:
    http://www.smartyouth.net/scheme.rar
    http://www.fortunesoft.sd.cn/web/UF/20041288501579904.rar相关知识使用 Microsoft .NET 的企业解决方案模式
    http://www.microsoft.com/china/msdn/architecture/patterns/Esp/.Text Blog源代码
    http://scottwater.com/DotText/default.aspx
      

  3.   

    这是篇不错的文章,再次感谢作者www.smartyouth.net 这是网站是作者的新成果,更近了一步
      

  4.   

    由于前段时间,我的空间出了问题,所以导致scheme.rar不能下载。我今晚会重新传一份上去。  我在这个月会发布一个最新的0.2 alpha版本,希望大家关注。
    比scheme.rar不知道要好多少倍了。呵呵。  只是由于目前工作的事情比较多,所以时间上一直比较紧。  希望大家到时候不要太介意。