【IT168 专稿】本篇我们将首先讨论用户接口母版页面的设计,然后重点讨论系统中主要用户接口页面的设计问题。
设计用户接口母版页面—Page.Master
我们想在所有的用户接口页面中实现一个一致的导航侧栏,对于我们的博客系统来说,此导航栏位于右边。这个导航栏将列出下列内容:
一段有关于当前博客的简短描述。
一个标签链接列表。这个列表中将仅显示至少关联到一个博客入口的标签。单击其中一个标签会把当前用户导航到相应于此标签的所有的博客入口。
一个文章链接列表。这个列表将显示标记为文章的博客入口的标题。一旦单击该标题,即打开相应的博客入口。
一个按降序排列的日期链接的列表。这个列表将显示发布博客入口对应的日期。一旦单击其中的某个月份,在该月份发布的所有的博客入口都将被显示。
一个用户可以订阅的RSS回馈的列表。
这个导航侧栏将会出现在所有的用户接口页面上。因此,我们需要在Page.Master母版页面中实现它。
导航侧栏中的第一个元素,即关于当前博客的描述,事实上是应用程序的配置文件web.config中的一个定制设置:
<add key="blogdescription" value="<h3>关于我!</h3><p>这里是你的博客描述。</p>" />
</appSettings>
在Page.Master文件的右侧,放置了一个标签控件。在Page_Load函数中,用博客描述信息来填充这个标签的值。请注意,此博客描述信息是使用BlogoSettings类从web.config文件中读取的。相应代码如下:
{
LabelBlogDescription.Text = BlogoSettings.BlogDescription;
}
接下来在侧栏中要实现的是三个链接区段,都是根据数据库内容生成的。实现这些内容有许多种方案,但是在我们的架构中一个颇为吸引人的选择是使用ASP.NET中新推出的ListView控件,还有另一个ObjectDataSource控件。ListView控件在控制生成的标签方面效率相当不错,而ObjectDataSource控件使我们能够把ListView控件数据绑定到业务逻辑层。
下面,我将详细地描述第一个区段的标签链接的实现过程。至于其他两个区段,其工作方式类似,读者可以自行在Page.Master文件的源代码中详细分析。
首先,在Page.Master文件中添加一个ObjectDataSource控件并且使用智能标签菜单配置它。在“选择业务对象”对话框中,从业务逻辑层选择TagManager类,并且勾选复选框控件“只显示数据组件”,如下图所示。
图1. ObjectDataSource组件的业务对象选择界面
在接下来的“定义数据方法”对话框中,针对Select语句选择GetListUsedTags方法。
图2. ObjectDataSource数据方法选择界面
注意,这个来自于业务逻辑层的方法看起来就象是在本地一样,这是因为它们修饰以特别的属性(已经在前面讨论过)。该GetListUsedTags方法来自于业务逻辑层,它负责返回一个至少存在一个博客入口的标签对象的列表。由于我们仅想显示这个标签列表,所以我们就不需要更新,插入或者删除等方法了。因此,我们可以单击“完成”按钮直接关闭配置向导。
接下来,我们在Page.Master文件中再添加一个ListView控件。然后,设置它的下列两个重要属性:
DataSourceID:我们需要把这个属性设置为我们刚刚创建的ObjectDataSource控件的ID。
ItemPlaceHolderID:把这个属性设置为itemContainer。
现在,既然我们配置好了ObjectDataSource和ListView控件,接下来我们就可以使用ListView控件的标记模板控制要输出的内容了。为此,我们使用了典型的静态标记与动态数据绑定方式的组合编码:
EnableViewState="False" ItemPlaceholderID="itemContainer">
<LayoutTemplate>
<h3>标签</h3>
<div class="content">
<ul class="linklist">
<li><a href="TagCloud.aspx">> 标签 <</a></li>
<asp:PlaceHolder ID="itemContainer" runat="server" />
</ul>
</div>
</LayoutTemplate>
<ItemTemplate>
<li><a href="BlogByTag.aspx?tag=<%# Eval("id") %>"><%#Eval("tagname") %></a></li>
</ItemTemplate>
<EmptyDataTemplate>
<h3>Tags</h3>
<div class="content">
<ul class="linklist">
<li>尚未提供标签。</li>
</ul>
</div>
</EmptyDataTemplate>
</asp:ListView>
在本文示例中,我们使用了ListView控件的三个不同的标记模板:
LayoutTemplate模板:这个模板生成标记的优异嵌套,它包含开始和结束标记。更为重要的是,在这个模板内部加入了itemContainer占位符控件,此控件将包含通过ItemTemplate模板生成的结果标记。
ItemTemplate模板:这个模板定义了针对每单个的标签需要生成的标记。易见,在上面的代码中综合使用了静态HTML和数据绑定标签属性(标签对象的id和tagname)。
EmptyDataTemplate模板:当不存在由ObjectDataSource控件返回的标签对象时使用这个模板。
在侧栏部分的按日期分布的文章和博客入口动态的链接列表也是以类似上面的方式工作:联合使用ListView和ObjectDataSource控件来配置要检索和显示的内容。
侧栏部分的最后一部分是一个用户可以订阅的回馈信息列表。这个列表是静态的,因此在Page.Master文件中可以使用简单的标记来实现,相应代码如下:
<div class="content">
<ul class="linklist">
<li><a id="RSSBlogs" href="~/View/RSS/RSSBlogs.aspx" runat="server">博客入口</a></li>
<li><a id="RSSArticles" href="~/View/RSS/RSSArticles.aspx" runat="server">文章</a></li>
<li><a id="RSSComments" href="~/View/RSS/RSSComments.aspx" runat="server">评论</a></li>
</ul>
</div>
【注意】通过把标签标记以runat="server",我们可以使用这一技巧方便地链接到我们的web应用程序的根目录下。
至此,我们完成了母版页面Page.Master的创建。你可能注意到,在这个母版页面中我们链接到了若干我们现在还未创建的.aspx页面。不必担心,稍后我们会讨论这些内容。
现在,我们可以正式构建我们的第一个用户接口页面。你会看到我们以前的努力将有助于我们迅速地开发这些页面。借助于我们的母版页面架构,我们将要构建的每一个新的用户接口页面主要的任务就是,使用标准ASP.NET控件结合我们需要的内容来填充内容栏目,而所有其它的内容会由系统以一种一致的方式自动生成。
至此,我们可以得出这样的结论:大多数的用户接口页面主要是列出按出版日期排序(默认的)的博客入口,标签和发表的文章。尽管对每一个博客概要信息页面来说排序及过滤方式可能有所不同,但是,以这种列表的方式显示一个博客入口的内容对于所有的这些页面是相同的。
考虑到上述情况,我们可以开发一个用户控件来减少标记及代码的重用。在ASP.NET开发中一个方便的重用相关联的标记和控件就是创建一个web用户控件。在博客案例中,Web用户控件BlogList.ascx位于方案的View/Controls文件夹下。该控件中包含了三个元素:
? 一个ListView—定义博客入口列表标记。
? 一个DataPager—负责实现ListView控件中博客入口的分页显示。
? 一个事件。该控件的后台代码文件中包含了一个代理方法ImplodeTags,由此方法调用当前页面的基页面中真正的ImplodeTags函数。在ASP.NET中,由于数据绑定复杂的对象的限制,所以需要这样的方法。
注意,ObjectDataSource控件并不是Web用户控件的一部分,因为对于每一个博客入口而言这一部分一般是不同的。为此,我们把Web用户控件内部的ListView控件隐式地链接到一个名字为ObjectDataSourceBlogs的ObjectDataSourceControl控件。
设计系统主页—Home.aspx
在构建了可重用的Web用户控件后,我们现在可以开始创建系统主页面。具体过程如下:
1. 创建一个新的ASPX页面,命名为Home.aspx。
2. 设置此新页面的母版页面属性指向Page.Master文件。
3. 删除Page标签下面所有的内容并插入ContentPlaceHolder标签。我们将把我们的内容填加到这个中间去。
4. 在ContentPlaceHolder标签内添加一个标签控件。在Page_Load函数中,用应用程序配置参数(由博客管理员来维护的,存储于文件web.config中)来填充这个标签的值。
5. 把web用户控件BlogList拖动到contentplaceholder标签内。
6. 添加一个ObjectDataSource控件,命名为“ObjectDataSourceBlogs”。配置它,使之使用业务逻辑层BlogEntryManager类中的GetList方法。确保ObjectDataSource控件支持分页功能,因为我们仅仅想在主页上显示最后三个博客入口(详见下面的代码)。
下面给出主页Home.aspx文件的标记代码,如下所示:
<%@ Register src="../Controls/BlogList.ascx" tagname="BlogList" tagprefix="uc1" %>
<asp:content id="ContentPlaceHolder" contentplaceholderid="ContentPlaceHolderPage" runat="server">
<form id="form" runat="server">
<div class="post">
<h3><asp:Label id="lblTitle" runat="server"></asp:Label></h3>
</div>
<uc1:BlogList ID="BlogList" runat="server" />
<asp:ObjectDataSource ID="ObjectDataSourceBlogs" runat="server"
OldValuesParameterFormatString="original_{0}" SelectMethod="GetList"
TypeName="BLOGO.NET.Business.BlogEntryManager" EnablePaging="True" SelectCountMethod="Count" StartRowIndexParameterName="StartRow" MaximumRowsParameterName="PageSize" EnableCaching="False" EnableViewState="False">
<SelectParameters>
<asp:QueryStringParameter Name="StartRow" QueryStringField="page"
Type="Int32" />
<asp:Parameter DefaultValue="3" Name="PageSize" Type="Int32" />
</SelectParameters>
</asp:ObjectDataSource>
</form>
</asp:content>
由于我们重用了BlogList控件并继承了母版页面,所以你会注意到其中仅使用了几行标记代码,并且大多数的标记代码结果都仅仅使用了配置方式,而不是编码方式。
上面解释的有关页面Home.aspx的工作方式与本系统中创建的如下几个博客页面非常类似:
BlogArticles.aspx。这个页面列出所有的标记为类型“article”的博客入口。
BlogByMonth.aspx。这个页面列出满足所有指定月份的博客入口。
BlogByTag.aspx。这个页面列出相应于一个指定标签的所有的博客入口。
在每一个页面中唯一不同的是ObjectDataSource控件的配置方式—从业务逻辑层选择适当的GetList重载,并且使用相应的经常是来自于querystring值的过滤参数,以及可能的header标签。因此,我们不再解释这些页面,但推荐读者自行分析之(位于View/Pages文件夹下)。
在介绍了博客概要信息相关页面后,还存在另外几个页面需要了解一下。
设计博客入口页面—BlogEntry.aspx
这个页面根据querystring指定的ID值显示一个特定的博客入口或文章。该页面使用一个ListView和ObjectDataSource来显示博客入口。因为每一个博客入口都可以看作是一个长度为1的列表,所以使用ListView控件来展示之是合适的。而且,这能够使我们重用我们的Web用户控件BlogList中的大量标记。既然在所有的情况下我们在这个页面上仅显示一个博客入口,所以就取消了分页支持。
紧接着上面的控件,页面中添加了另一对ListView控件和ObjectDataSource控件—此控件与业务逻辑层的CommentManager类相关联。作为输入参数,这里使用了唯一的标识博客入口的querystring参数ID,以便检索和显示仅仅与该博客入口相应的评论。
页面的最下面显示的是一个子表单,用于提供给用户针对当前博客入口提交新的评论内容。这个表单是使用标准ASP.NET Web控件构建的,例如TextBox控件,按钮控件以及强制进行输入域校验的RequiredFieldValidator控件。该新的评论表单使用一个PlaceHolder控件包围起来。这可以使我们能够在运行时刻根据需要启用或禁止使用它。这是非常有必要的,因为博客系统管理员可能会决定每一个博客入口是否允许予以添加评论。在表单的Page_Load事件中包含了这一代码,通过使用PlaceHolder控件的Visible属性值动态地控制显示或者隐藏这个新的评论子表单。
后台代码文件中特别重要的是与评论表单的提交按钮的Click事件相关联的部分。到现在为止,我们一直使用标准ASP.NET控件绑定到我们的业务逻辑层。然而,在这部分代码中,我们显式地使用业务逻辑层把新的评论保存到数据库。相关代码如下:
//……省略其他代码
Comment com = new Comment();
com.author = Server.HtmlEncode(CommentName.Text);
// 1.为了防止可能存在恶意用户把HTML代码注入到评论中,我们将对评论内容进行专门的编码处理
// 2.为了保留用户输入的换行符,我们使用HTML标记元素<br/>来替换\r字符
com.body = Regex.Replace(Server.HtmlEncode(CommentBody.Text), @"\r", "<br/>", RegexOptions.Multiline);
com.blog_id = long.Parse(Request.QueryString["page"]);
com.datecreated = System.DateTime.Now;
com.datemodified = System.DateTime.Now;
com.IP = Request.UserHostAddress;
CommentManager.Save(com);
请注意上面的代码中我们是怎样使用以前定义的业务逻辑层的代码把一个新的评论插入到数据库中的。
设计文件页面—File.aspx
文件页面File.aspx在本系统中具有特殊作用。它的目的是用于显示一个querystring参数中的ID指定的文件的二进制内容。因此,我们需要人工干预ASP.NET的标准HTTP响应对象,修改它的MIME类型,使之能够显示二进制数据(即图像数据)。
在文件File.aspx的Page_Load事件中,下列代码负责实现上述功能:
设计标签云页面—TagCloud.aspx
{
//检查是否传递了文件标识符
if (Request.QueryString["file"] != null)
{
//取得文件对象
long fileID = long.Parse(Request.QueryString["file"].ToString());
BLOGO.NET.Business.File myFile = FileManager.GetItem(fileID);
if (myFile != null) {
//在数据库中发现一个文件对象,则输出此文件的内容
Response.Clear();
Response.ContentType = myFile.mime;
Response.OutputStream.Write((byte[])myFile.filecontent, 0, (int)myFile.filecontent.Length);
Response.End();
}
else
{
//没有找到与指定的文件标识符相匹配的文件
throw new Exception("Invalid file identifier: " + fileID.ToString());
}
}
}
TagCloud页面负责列出博客中使用的标签链接并且根据每一个标签中博客入口的数目调整这些链接的大小。显然,目前并不存在标准的ASP.NET控件来完成这样的任务;因此,我们使用下面的方案来实现上述功能:
该页面包含一个标签控件。
后台代码文件的Page_Load事件中包含生成标签云的逻辑。为此,我们不但使用了业务逻辑层的TagManager类,而且还专门创建了一些计算公式来分布标签的权重(重要性)。
层叠样式表文件View\CSS\default.css中包含了预定义的标签大小元素,用于决定每一个标签的字体的大小。
总结
在本部分中,我们首先讨论用户接口母版页面的设计,然后重点讨论系统中主要用户接口页面的设计问题。在接下来的第五篇中,我们将讨论了系统中主要管理页面设计的设计问题。