User Control大家肯定不会陌生,在使用ASP.NET的过程中,除了aspx页面,最常见的就莫过于ascx了。ascx是一个有独立逻辑的组件,提供了强大的复用特性,合理使用,能够大大提高开发效率。通过User Control直接生成HTML内容其实已经是一个比较常用的技巧了(尤其在AJAX时代),不过网络上这方面的内容比较少,很多人还是在苦苦地拼接字符串,因此在这里我通过一个实例简单介绍一下这个技巧。
对一个对象(文章,图片,音乐,etc.)进行评论是应用中最常见的功能之一。首先,我们定义一个Comment类,以及其中会用到的“获取”方法:
public partial class Comment{ public DateTime CreateTime { get; set; } public string Content { get; set; }} public partial class Comment{ private static List<Comment> s_comments = new List<Comment> { new Comment { CreateTime = DateTime.Parse("2007-1-1"), Content = "今天天气不错" }, new Comment { CreateTime = DateTime.Parse("2007-1-2"), Content = "挺风和日丽的" }, new Comment { CreateTime = DateTime.Parse("2007-1-3"), Content = "我们下午没有课" }, new Comment { CreateTime = DateTime.Parse("2007-1-1"), Content = "这的确挺爽的" } }; public static List<Comment> GetComments(int pageSize, int pageIndex, out int totalCount) { totalCount = s_comments.Count; List<Comment> comments = new List<Comment>(pageSize); for (int i = pageSize * (pageIndex - 1); i < pageSize * pageIndex && i < s_comments.Count; i++) { comments.Add(s_comments[i]); } return comments; }}
为了显示一个评论列表,我们可以使用一个用户控件(ItemComments.aspx)来封装。自然,分页也是必不可少的:
<asp:Repeater runat="server" ID="rptComments"> <ItemTemplate> 时间:<%# (Container.DataItem as Comment).CreateTime.ToString() %><br /> 内容:<%# (Container.DataItem as Comment).Content %> </ItemTemplate> <SeparatorTemplate> <hr /> </SeparatorTemplate> <FooterTemplate> <hr /> </FooterTemplate></asp:Repeater> <% if (this.PageIndex > 1) { %> <a href="/ViewItem.aspx?page=<%= this.PageIndex - 1 %>" title="上一页">上一页</a> <% } %> <% if (this.PageIndex * this.PageSize < this.TotalCount) { %> <a href="/ViewItem.aspx?page=<%= this.PageIndex + 1 %>" title="上一页">下一页</a><% } %>
还有:
public partial class ItemComments : System.Web.UI.UserControl{ protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); this.rptComments.DataSource = Comment.GetComments(this.PageSize, this.PageIndex, out this.m_totalCount); this.DataBind(); } public int PageIndex { get; set; } public int PageSize { get; set; } private int m_totalCount; public int TotalCount { get { return this.m_totalCount; } }}
然后再页面(ViewItem.aspx)中使用这个组件:
<div id="comments"><demo:ItemComments ID="itemComments" runat="server" /></div>
以及:
public partial class ViewItem : System.Web.UI.Page{ protected void Page_Load(object sender, EventArgs e) { this.itemComments.PageIndex = this.PageIndex; } protected int PageIndex { get { int result = 0; Int32.TryParse(this.Request.QueryString["page"], out result); return result > 0 ? result : 1; } }}
打开ViewItem.aspx之后效果如下:
这张页面的功能非常简单,那就是察看评论。当前评论的页码会使用QueryString的page项进行指定,然后在ViewItem.aspx里获取到并且设置ItemComments.ascx控件的属性。ItemComments控件会根据自身属性来获取数据,进行绑定,至于显示内容,全都定义在ascx中了。由于需要分页功能,这个评论控件中还包含了上一页和下一页的链接,他们链接的目标很简单,就是ViewItem.aspx页,并且加上页码的Query String而已。
功能是完成了,不过用着用着忽然觉得不妥,为什么呢?因为我们在翻页,或者用户发布评论的时候,整张页面都刷新了。这可不好,要知道可能ViewItem页中还有其他几个显示部分,它们可是不变的。而且如果其他几个部分也需要分页,那么可能就需要保留页面上每一部分的当前页码,这样开发的复杂性还是比较高的。
那么我们不如用AJAX吧。无论是用户察看评论时进行翻页还是发表评论,都不会对页面上的其他内容造成影响。要开发这个功能,自然需要服务器端的支持,那么该怎么做呢?一般我们总是有两种选择:
- 服务器端返回JSON数据,在客户端操作DOM进行呈现。
- 服务器端直接返回HTML内容,然后在客户端设置容器(例如上面id为comments的div)。
不过无论采用哪种做法,“呈现”的逻辑一般总是另写一遍(第一次的呈现逻辑写在了ItemComments.ascx中)。如果使用第1种做法,那么呈现逻辑就需要在客户端通过操作DOM进行呈现;如果使用第2种做法,那么就要在服务器端进行字符串拼接。无论哪种做法都违背了DRY原则,当ItemComments.ascx里的呈现方式修改时,另一处也要跟着修改。而且无论是操作DOM元素还是拼接字符串维护起来都比较麻烦,开发效率自然也就不高了。
如果我们能够直接从ItemComments控件获得HTML内容该多好啊——那么我们就这么做吧。请看如下代码(GetComments.ashx):
public class GetComments : IHttpHandler{ public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/plain"; ViewManager<ItemComments> viewManager = new ViewManager<ItemComments>(); ItemComments control = viewManager.LoadViewControl("~/ItemComments.ascx"); control.PageIndex = Int32.Parse(context.Request.QueryString["page"]); control.PageSize = 3; context.Response.Write(viewManager.RenderView(control)); } public bool IsReusable { ... }}
很简单的代码,不是吗?创建对象,设置属性,然后通过Response.Write输出而已。实在没什么大不了的——不过关键就在于ViewManager类,我们来看一下它是怎么实现的:
public class ViewManager<T> where T : UserControl{ private Page m_pageHolder; public T LoadViewControl(string path) { this.m_pageHolder = new Page(); return (T)this.m_pageHolder.LoadControl(path); } public string RenderView(T control) { StringWriter output = new StringWriter(); this.m_pageHolder.Controls.Add(control); HttpContext.Current.Server.Execute(this.m_pageHolder, output, false); return output.ToString(); }}
ViewManager中只有两个方法:LoadViewControl和RenderView。LoadViewControl方法的作用是创建一个Control实例并返回,RenderView方法的作用则就是生成HTML了。这个实现方式的技巧在于使用了一个新建的Page对象作为生成控件的“容器”,而最后其实我们是将Page对象的整个生命周期运行一遍,并且将结果输出。由于这个空的Page对象不会产生任何其他代码,因此我们得到的,就是用户控件生成的代码了。
不过要实现这个AJAX效果,还需要做两件事情。
第一,就是简单修改一下ItemComments控件中的翻页链接,让它被点击时调用一个JavaScript函数。例如“上一页”的代码就会变成:
<a href="/ViewItem.aspx?page=<%= this.PageIndex - 1 %>" title="上一页" onclick="return getComments(<%= this.PageIndex - 1 %>);">上一页</a>
第二,就是实现getComments这个客户端方法。在这里我使用了prototype框架,好处就是能够用相当简洁的代码来做到替换HTML的AJAX效果:
<script type="text/javascript" language="javascript"> function getComments(pageIndex) { new Ajax.Updater( "comments", "/GetComments.ashx?page=" + pageIndex + "&t=" + new Date(), { method: "get" }); return false; // IE only }</script>
大功告成。
其实就像之前所说的那样,使用UserControl进行HTML代码生成是一个十分常用的技巧。尤其在AJAX应用越来越普及的情况下,合理使用上面提到的方式可以方便的为我们的应用添加AJAX效果。而且很多情况下,我们即使不需要在页面上显示内容,也可以将内容使用UserControl进行编辑。因为编写UserControl比拼接字符串的方式无论是在开发效率上还是可维护性上都高出许多。由于这个方式其实使用了WebForms这个久经考验的模型,因此在执行效率方面也是相当高的。此外,就刚才的例子来说,使用UserCotrol进行HTML生成还有其他好处:
- 页面呈现逻辑只实现了一次,提高了可维护性。
- 不会影响页面的SEO,因为在客户端<a />的href还是有效的。
事实上,WebForms是一个非常强大的模型,所以ASP.NET MVC的View也使用了WebForms的引擎。通过上面这个例子,我们其实还可以做到其他很多东西——例如用UserControl来生成XML数据,因为UserControl本身不会带来任何额外的内容。