1.3  URL重写功能

URL重写功能就是接受带有有效命名约定的URL,把它们转化为查询字符串。需要有效命名约定的两个原因是:将信息组织到逻辑层次结构中,以及隐藏查询字符串参数。本节将说明URL重写功能如何改进用户界面,描述实现URL重写功能的新旧方式,并给出一些代码来演示这个概念。
注意:
本节还添加了一些代码,来演示n层体系结构和数据绑定过程中的最佳实践方式,而不是使用较简单的数据源控件。


1.3.1  为什么要重写URL

看看博客是如何按时间组织的,就可以明白分层组织的含义。从用户的角度来看,下面的查询字符串是很难理解的:
http://www.someblogsite.com/username/?y=2005&m=01&d=31
上面的查询字符串返回2005年1月1日的博客项,这并不容易确定。我们可以修改代码,使其参数更有意义,如下:
http://www.someblogsite.com/username/?year=2005&month=01&day=31
无论查询字符串的参数如何表示,一般用户都很难理解它。最好用富有层次感的表示方式编写查询字符串,如下:
http://www.someblogsite.com/username/2005/January/31
对于站点的一般访问者,上面的URL不是很难理解。例如,如果用户删除了日期,就会得到1月的所有记录。
即使没有自然的层次结构或干脆没有参数,只要URL有一个有意义的名称,而不是使用查询参数,用户还是较容易理解它们的。

1.3.2  ASP.NET v1.1的窍门程序

在ASP.NET v1.x中执行URL重写功能的一个很好的资源是Scott Mitchell撰写的MSDN文章“URL Rewriting in ASP.NET”,它位于msdn.microsoft.com/library/default.asp?url= /library/en-us/dnaspp/html/urlrewriting.asp。在这篇文章中,Scott解释了如何通过HTTP模块和HTTP处理程序执行URL重写功能,并说明了它们的使用场合。他还建立了一个可重用的URL重写引擎,通过配置文件使用正则表达式。

1.3.3  ASP.NET v2.0 的替代品

在ASP.NET v2.0中,是通过urlMappings配置元素支持重写URL功能的。给web.config添加一个新项,来映射URL,如下所示:
<urlMappings enabled="true">
  <add url="~/Articles/AspDotNet/UrlRewriting"
mappedUrl="~/Articles.aspx?cat=1&id=16" />
</urlMappings>
用户看到的是url特性,而mappedUrl特性描述了实际请求的页面。在上面的urlMappings元素中,假定有一个文章页面根据类别和文章标识符动态返回文章。于是,url特性会显示首选的用户界面,但mappedUrl特性会显示实际的页面和请求的参数。

1.3.4  实现URL映射功能

以前笔者编写过一个示例应用程序,来说明如何使用这个功能。这是文章的一个变体,介绍了上述概念,但它是根据年份和月份来解释的。例如,文章应用程序允许选择年份,再选择月份。每个页面上都显示了可读的URL,我们可以利用该URL,简单地修改页面地址,来导航应用程序。程序清单1-6显示了文章应用程序的初始页面。
程序清单1-6  使用可读的URL标识年份的主页: Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs"
Inherits="_Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>.NET Article Archive</title>
</head>
<body>
    <form id="form1" runat="server">
        <h1>.NET Article Archive</h1>
        <p>
            Pick a Year:
        </p>
        <p>
            <asp:HyperLink ID="HyperLink1" runat="server"
                 NavigateUrl="~/2006">2006</asp:HyperLink><br />
            <asp:HyperLink ID="HyperLink2" runat="server"
                 NavigateUrl="~/2005">2005</asp:HyperLink>
        </p>
    </form>
</body>
</html>
HyperLink元素的NavigateUrl特性包含可读的URL,该URL要重写为查询字符串参数。同样,在用户选择一个年份后,就会看到如程序清单1-7所述的年份页面。
程序清单1-7  带有可读URL的年份页面: YearView.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="YearView.aspx.cs"
Inherits="YearView" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Articles for the Year</title>
</head>
<body>
    <form id="form1" runat="server">
        <h1>
            <asp:Label ID="lblTitle"
                       runat="server"
                       Text="Articles for the Year #">
            </asp:Label>
        </h1>
        <p>
            <asp:Panel ID="pnlMonths" runat="server" Height="50px"
Width="125px">
                <asp:HyperLink ID="hypJanuary"   runat="server"
                    NavigateUrl="~/YEAR/01">January</asp:HyperLink><br />
                <asp:HyperLink ID="hypFebruary"  runat="server"
                    NavigateUrl="~/YEAR/02">February</asp:HyperLink><br />
                <asp:HyperLink ID="hypMarch"     runat="server"
                    NavigateUrl="~/YEAR/03">March</asp:HyperLink><br />
                <asp:HyperLink ID="hypApril"     runat="server"
                    NavigateUrl="~/YEAR/04">April</asp:HyperLink><br />
                <asp:HyperLink ID="hypMay"       runat="server"
                    NavigateUrl="~/YEAR/05">May</asp:HyperLink><br />
                <asp:HyperLink ID="hypJune"      runat="server"
                    NavigateUrl="~/YEAR/06">June</asp:HyperLink><br />
                <asp:HyperLink ID="hypJuly"      runat="server"
                    NavigateUrl="~/YEAR/07">July</asp:HyperLink><br />
                <asp:HyperLink ID="hypAugust"    runat="server"
                    NavigateUrl="~/YEAR/08">August</asp:HyperLink><br />
                <asp:HyperLink ID="hypSeptember" runat="server"
                    NavigateUrl="~/YEAR/09">September</asp:HyperLink><br />
                <asp:HyperLink ID="hypOctober"   runat="server"
                    NavigateUrl="~/YEAR/10">October</asp:HyperLink><br />
                <asp:HyperLink ID="hypNovember"  runat="server"
                    NavigateUrl="~/YEAR/11">November</asp:HyperLink><br />
                <asp:HyperLink ID="hypDecember"  runat="server"
                    NavigateUrl="~/YEAR/12">December</asp:HyperLink>
            </asp:Panel>
        </p>
    </form>
</body>
</html>
程序清单1-7显示了HyperLink元素,其NavigateUrl特性设置为可读的URL。它为一年中的每个月包含一层,其中数字对应月份的顺序。为了正确处理这些URL,应编辑Web窗体YearView的代码文件,如程序清单1-8所示。
程序清单1-8  通过参数化的URL读取页面的查询字符串参数: YearView.aspx.cs
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class YearView : System.Web.UI.Page
{
    string year;  // passed in query string
    protected void Page_Load(object sender, EventArgs e)
    {
        // we'll use this in multiple methods
        year = Request.QueryString["year"];
        // set page title
        SetTitle();
        // configure months to refer to proper page
        SetMonths();
    }
    /// <summary>
    /// configure months to refer to proper page
    /// </summary>
    private void SetMonths()
    {
        foreach (Control ctrl in pnlMonths.Controls)
        {
            HyperLink monthLink = ctrl as HyperLink;
            if (monthLink != null)
            {
                monthLink.NavigateUrl =
                    monthLink.NavigateUrl.Replace("YEAR", year);
            }
        }
    }
    /// <summary>
    /// set page title
    /// </summary>
    private void SetTitle()
    {
        lblTitle.Text = "Articles for the Year " + year;
    }
}
程序清单1-8中的Page_Load方法从查询字符串中提取year参数,使之可用于后续的方法。SetTitle方法使用这个值重写有正确年份的页面标题。
另外还要注意程序清单1-8中HyperLink控件的NavigateUrl属性,它们在URL的年份位置上都包含“YEAR”文本。这会使代码更富有动感,因为根据年份,这些NavigateUrl属性必须重写。这就是程序清单1-8中SetMonths方法的作用。程序清单1-8中的代码故意把每个HyperLink控件放在一个面板中,以便在代码中使用其Controls集合。因此,在代码文件中,SetMonths方法可以迭代Controls集合,用一个简单的string.Replace方法调用来设置年份。为了使这个内置的URL重写功能可用于这个应用程序,web.config文件中有一个urlMapping元素,如程序清单1-9所示。
程序清单1-9  web.config文件中的urlMapping元素可以在ASP.NET v2.0中重写URL
<?xml version="1.0"?>
<configuration>
  <system.web>
    <urlMappings>
      <add url="~/2006"
           mappedUrl="~/YearView.aspx?year=2006"/>
      <add url="~/2006/01"
           mappedUrl="~/MonthView.aspx?year=2006&amp;month=01"/>
      <add url="~/2006/02"
           mappedUrl="~/MonthView.aspx?year=2006&amp;month=02"/>
      <add url="~/2005"
           mappedUrl="~/YearView.aspx?year=2005"/>
      <add url="~/2005/01"
           mappedUrl="~/MonthView.aspx?year=2005&amp;month=01"/>
      <add url="~/2005/02"
           mappedUrl="~/MonthView.aspx?year=2005&amp;month=02"/>
    </urlMappings>
    <compilation debug="true"/>
  </system.web>
</configuration>
在程序清单1-9的每个add元素中,可重写的URL转化为mappedUrl,mappedUrl是实际的地址,也是发送给页面的查询字符串。发音符号(~)表示每个页面相关的应用程序目录。必须使用&amp;替代&符号,将参数分隔开。这里省略了月份,以缩短程序清单。
要查看文章列表,用户选择了他感兴趣的月份。程序清单1-10演示了MonthView.aspx的执行过程。在程序清单1-10中使用了GridView控件,是因为这里需要为多个数据行格式化输出,并把它绑定到ObjectDataSource控件上。GridView控件是ASP.NET v2.0中的新增控件,它替代了DataGrid控件。
程序清单1-10  使用GridView读取查询参数:MonthView.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="MonthView.aspx.cs"
Inherits="MonthView" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Articles for the Month</title>
</head>
<body>
    <form id="form1" runat="server">
        <h1>
            Requested Articles:</h1>
        <br />
        <asp:GridView ID="GridView1" runat="server"
            AutoGenerateColumns="False" DataSourceID="ArticlesODS">
            <Columns>
                <asp:BoundField DataField="Year"
                    HeaderText="Year" SortExpression="Year" />
                <asp:BoundField DataField="Month"
                    HeaderText="Month" SortExpression="Month" />
                <asp:BoundField DataField="Title"
                    HeaderText="Title" SortExpression="Title" />
                <asp:BoundField DataField="Content"
                    HeaderText="Content" SortExpression="Content" />
            </Columns>
        </asp:GridView>
        <asp:ObjectDataSource ID="ArticlesODS" runat="server"
            SelectMethod="GetArticles" TypeName="Articles">
            <SelectParameters>
                <asp:QueryStringParameter DefaultValue="2006"
                    Name="year" QueryStringField="year"
                    Type="String" />
                <asp:QueryStringParameter DefaultValue="01"
                    Name="month" QueryStringField="month"
                    Type="String" />
            </SelectParameters>
        </asp:ObjectDataSource>
    </form>
</body>
</html>
为了提倡设计良好的n层体系结构,这里使用业务对象和正式的数据访问层来实现文章的管理。因此在MonthView.aspx页面中使用了ObjectDataSource控件。很容易把查询字符串参数直接映射到Articles类的GetArticles方法上,如程序清单1-11所示。
程序清单1-11  Articles类包含一组article对象: articles.cs
using System;
using System.Collections.Generic;
/// <summary>
/// List of Articles
/// </summary>
public class Articles : List<Article>
{
    public List<Article> GetArticles(string year, string month)
    {
        ArticleData dal = new ArticleData();
        dal.GetArticles(this, year, month);
        return this;
    }
}
程序清单1-11中的Articles类利用C#编程语言中新增的泛型特性,创建了一个强类型化的Article对象集合。通过继承List<Article>,可以获得泛型的优势,给类型指定更容易理解的名称。程序清单1-12列出了用ArticleData类表示的数据访问层。
程序清单1-12  ArticleData类用数据源中的新文章填充当前的列表:ArticleData.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Web;
/// <summary>
/// Summary description for ArticleData
/// </summary>
public class ArticleData
{
    public void GetArticles(List<Article> articles, string year, string
month)
    {
        DataSet dsArticles = new DataSet();  
dsArticles.ReadXml(HttpContext.Current.Server.MapPath("Articles
.xml"));
        DataView dvArticles = new DataView(dsArticles.Tables["article"]);
        dvArticles.RowFilter =
            "year = '" + year + "' " +
            "and month = '" + month + "'";
        Article currArticle = null;
        IEnumerator articleRows = dvArticles.GetEnumerator();
        while (articleRows.MoveNext())
        {
            DataRowView articleRow = (DataRowView)articleRows.Current;
            currArticle = new Article(
                (string)articleRow["year"],
                (string)articleRow["month"],
                (string)articleRow["title"],
                (string)articleRow["content"]);
            articles.Add(currArticle);
        }
    }
}
这个类把一个XML文件(参见程序清单1-13)加载到DataSet中。它根据所传送的年份和月份参数,使用DataView过滤结果。Article类是一个业务对象,所以最终在UI层中绑定到GridView上。当然,UI层不需要知道数据是来自于XML文件或通过ADO.NET组件进行了处理。它很容易在以后进行修改,以满足新的要求。另外,GridView可以在Generic集合中显示业务对象,因此,我们通过articles参数把数据作为List<Article>传送回。
程序清单1-13  文章数据通过XML文件来表示: Articles.xml
<?xml version="1.0" encoding="utf-8" ?>
<articles>
  <article>
    <year>2005</year>
    <month>01</month>
    <title>Title1</title>
    <content>This is the text of Title1.</content>
  </article>
  <article>
    <year>2005</year>
    <month>02</month>
    <title>Title2</title>
    <content>This is the text of Title2.</content>
  </article>
  <article>
    <year>2005</year>
    <month>02</month>
    <title>Title3</title>
    <content>This is the text of Title3.</content>
  </article>
  <article>
    <year>2006</year>
    <month>01</month>
    <title>Title4</title>
    <content>This is the text of Title4.</content>
  </article>
  <article>
    <year>2006</year>
    <month>01</month>
    <title>Title5</title>
    <content>This is the text of Title5.</content>
  </article>
  <article>
    <year>2006</year>
    <month>02</month>
    <title>Title6</title>
    <content>This is the text of Title6.</content>
  </article>
</articles>
程序清单1-12中的代码把一个XML文件(参见程序清单1-13)用作数据源,完成了一个非常简单的实践。ArticlesData类使用该数据填充Article对象,如程序清单1-14所示。
程序清单1-14  Article类是一个业务对象,它要绑定到UI层的GridView上: Article.cs
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
/// <summary>
/// Represents an Article
/// </summary>
public class Article
{
    private string m_year;
    public string Year
    {
        get { return m_year; }
        set { m_year = value; }
    }
    private string m_month;
    public string Month
    {
        get { return m_month; }
        set { m_month = value; }
    }
 
    private string m_title;
    public string Title
    {
        get { return m_title; }
        set { m_title = value; }
    }
    private string m_content;
    public string Content
    {
        get { return m_content; }
        set { m_content = value; }
    }
 public Article(string year, string month, string title, string content)
 {
        Year = year;
        Month = month;
        Title = title;
        Content = content;
 }
}
Articles类中的数据通过属性表示为其公共接口,具有封装特性。这将允许在需要时改变底层实现方式,包括添加业务规则和验证逻辑。
注意:
如果使用数据源控件来替代上述代码中的ObjectDataSource控件,就不能添加业务规则了。不使用数据源控件是为了简化这个例子中的代码,它还有另一个好处。正确使用Object DataSource控件,可以设计应用程序,使其具备灵活性和可维护性。
使用ASP.NET v2.0的URL映射特性,似乎有很大的优势,但它有一个缺陷:不能使用正则表达式。本节中的文章示例使用正则表达式,将得到更好的效果。注意,urlMappings元素包含的代码非常类似,只能通过年份或月份来区分。通过一个正则表达式,用参数映射年份或月份,可以更好地实现这个功能。如果使用Scott Mitchell的URL重写引擎,配置将如下所示:
<RewriterConfig>
   <Rules>
      <!-- Rules for Blog Content Displayer -->
      <RewriterRule>
         <LookFor>~/(\d{4})/(\d{2})/Default\.aspx</LookFor>
         <SendTo><![CDATA[~/YearView.aspx?year=$1&month=$2]]></SendTo>
      </RewriterRule>
      <RewriterRule>
         <LookFor>~/(\d{4})/Default\.aspx</LookFor>
         <SendTo>~/YearView.aspx?year=$1</SendTo>
      </RewriterRule>
   </Rules>
</RewriterConfig>
这是一个简单的一次性的配置。但在Visual Studio 2005实现方式(参见程序清单1-9中配置文件的urlMapping元素)中,必须为每个年份和每个月份更新该配置文件。这是不切实际的,因为这个文件会变大,且需要手动干预。对于有正则表达式的URL重写功能,建议使用Scott Mitchell文章中提及的窍门程序。ASP.NET v2.0的URL映射特性只能在最简单的情况下使用。

1.4  小结

本章介绍了ASP.NET v1.1的先驱发明的几个窍门程序。在Microsoft把这些窍门程序转变为ASP.NET v2.0的主要产品特性时,这些人有很大的影响。Wizard窍门程序变成Wizard控件。模板、继承和用户控件Master Page窍门程序变成ASP.NET v2.0中的Master Page。URL重写窍门程序影响了ASP.NET v2.0中的URL映射,在需要正则表达式时,URL重写窍门程序一直以来都是实现URL重写功能的首选方法。