1. 引言

在前几次任务里开发的星级控件仅适用于静态展示,例如标明某个软件的受欢迎度,但是实际上很多网站还希望能够由用户对某一信息进行评分,最终计算出该信息的受欢迎程度,使数据更为客观和可信,由此需要在原有的星级控件上加以改进,使用户能够动态评分,实际的效果图看起来如下图所示:

图中第一行是经过评分后控件的状态,开发人员处理了评分事件并在页面输出了选择的分数;图中第二行显示了另外一种评分状态——鼠标移动到了星形图案上,此时使用红色的星形提示用户。

2. 分析

对于该控件我们要在原有控件的基础上加入两个特性:

1. 加入鼠标悬浮指示,当鼠标悬停时显示出用户选择的分数。

2. 在鼠标点击时能够回发到服务器并将相应事件暴露出来由开发人员处理。

对于第一个需求,可以处理JavaScript中的鼠标事件,当触发onmouseover事件时判断当前鼠标悬停在第几个星形图案,接着显示红色星形图案。显示图案时同样有一些技巧,在原有显示星形图案的层(div)中嵌套层,并将该层的背景设置为红色星形图案,在页面加载的时候该层不应该被显示出来(设置宽度为0即可),并且在鼠标悬停时设置该层的宽度,我们只需要注意能够使该层将背景层覆盖即可。

同样要考虑的是,在鼠标移出的时候需要将该层隐藏起来,那么只需要处理onmouseout事件,将层的宽度再次设置为0即达到了隐藏层的目的。

对于第二个需求,首先要在自定义控件中暴露一个公开事件使开发人员能够订阅该事件,接下来就是在客户端产生一个回发脚本,使得在点击(JavaScript中的onclick事件触发)时执行此回发脚本提交到服务器即可。

为了产生回发脚本,使自定义控件实现IPostBackEventHandler接口,该接口定义了ASP.NET服务器控件为处理回发事件而必须实现的方法。

在提交到服务器之后,自定义控件调用公开的事件,并且需要星形图案替换为黄色背景标识用户评分,那么我们同样可以在背景层中再加入一个层,使用和第一种需求相同的算法将背景层覆盖就可以了。

需要确认的是,在用户评分之后就不允许再次评分了(将用户当前鼠标悬停选择评分的层隐藏起来即可)。

最后要考虑的问题是,自定义控件中使用了服务器端控件作为容器显示图片,为了避免页面上放置多个自定义控件发生问题,需要保证此时各服务器端控件生成的客户端编号唯一,您一定已经想到了解决方法,就是实现INamingContainer接口。

3. 实现

3.1 在ControlSolution解决方案的ControlLibrary类库中创建继承自WebControl的PostStart类,并使其实现IPostBackEventHandler和INamingContainer接口:

using System;
using System.ComponentModel;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
  namespace ControlLibrary
{
     public class PostStar :
     WebControl,IPostBackEventHandler,INamingContainer
     {
     }
}

3.2 为自定义控件添加得分和注释属性:

[DefaultValue(0)]
public int Score
{
     get
     {
         object obj = ViewState["Score"];
         return obj == null ? 0 : Convert.ToInt32(obj);
     }
     internal set
     {
         ViewState["Score"] = value;
     }
}
  public string Comment
{
     get
     {
         object obj = ViewState["Comment"];
         return obj == null ? string.Empty : Convert.ToString(obj);
     }
     set
     {
         ViewState["Comment"] = value;
     }
}
3.3 重写CreateChildControls方法创建子控件层次,在该方法中调用了CreateControlHierarchy方法:
protected override void CreateChildControls()
{
     base.CreateChildControls();
      CreateControlHierarchy();
}

3.4 编写CreateControlHierarchy方法,在该方法中首先在子控件集合中创建了一个一行两列的表格,接下来调用CreateComment方法在第一页中输出注释,然后调用了CreateStart方法创建星形图案:

protected virtual void CreateControlHierarchy()
{
     Table table = new Table();
     TableRow row = new TableRow();
     table.Rows.Add(row);
     TableCell comment = new TableCell();
     CreateComment(comment);
     row.Cells.Add(comment);
      TableCell stars = new TableCell();
      CreateStars(stars);
      row.Cells.Add(stars);
      this.Controls.Add(table);
}
3.5 实现CreateComment方法,简单的将注释文本赋值给单元格的Text属性:
private void CreateComment(TableCell cell)
{
     cell.Text = Comment;
}

3.6 编写创建星形图案的CreateStars方法,在该方法中调用了RegisterCSS方法向页面注册使用的CSS样式表文件(该文件作为资源文件发布),接下来分别调用了CreateBackPanel、CreateCurrentPanel、和CreateChangePanel方法用于创建背景层、标识选中以后的层和表示当前鼠标悬停的层,而且还调用了CreateList方法以创建列表,最后按照层次结构将层和列表组织起来(使用Panel控件表示层):

private void CreateStars(TableCell cell)
{
     RegisterCSS();
      string starPath = Page.ClientScript.GetWebResourceUrl(this.GetType(), "ControlLibrary.Image.stars.gif");
      Panel panBg = CreateBackPanel(starPath); 
      cell.Controls.Add(panBg);
      Panel panCur = CreateCurrentPanel(starPath); 
     Panel panChange =CreateChangePanel(starPath);
     HtmlGenericControl ul = CreateList();
      panBg.Controls.Add(ul);
      panBg.Controls.Add(panCur);
     panCur.Controls.Add(panChange); 
}

3.7 实现RegisterCSS方法,取得样式表资源文件并使用HtmlLink类注册样式表:

private void RegisterCSS()
{
     string css = Page.ClientScript.GetWebResourceUrl(this.GetType(),
      "ControlLibrary.CSS.star.css");
      HtmlLink link = new HtmlLink();
     link.Href = css;
     link.Attributes.Add("rel", "stylesheet");
     link.Attributes.Add("type", "text/css");
      Page.Header.Controls.Add(link);
}

3.8 编写CreateBackPanel、CreateCurrentPanel、CreateChangePanel和CreateList方法实现:

private Panel CreateBackPanel(string starPath)
{
     Panel panBg = new Panel();
      panBg.ID = "divBg";
     panBg.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath);
          panBg.CssClass = "stars";
      return panBg;
}
  private Panel CreateCurrentPanel(string starPath)
{
     Panel panCur = new Panel();
      panCur.ID = "divCur";
     panCur.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath);
     panCur.CssClass = "current";
      return panCur;
}
  private Panel CreateChangePanel(string starPath)
{
     Panel panChange = new Panel();
      panChange.ID = "divChange";
     panChange.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath);
          panChange.CssClass = "change";
      return panChange;
}
  private HtmlGenericControl CreateList()
{
     HtmlGenericControl ul = new HtmlGenericControl("ul");
      ul.ID = "ulist";
          ul.Attributes.Add("class", "ulist");
      for (int i = 0; i < 5; i++)
     {
         HtmlGenericControl li = new HtmlGenericControl("li");
         li.Attributes.Add("value", (i + 1).ToString());
                                  ul.Controls.Add(li);
     }
      return ul;
}
3.10 重写Render方法呈现控件,该方法调用了PrepareControlForReader方法:
protected override void Render(HtmlTextWriter writer)
{
     PrepareControlForRender();
      base.Render(writer);
}

3.11 实现PrepareControlForRender,在该方法中按照控件层次取出列表中的列表项并注册JavaScript事件:

private void PrepareControlForRender()
{
     if (this.Controls.Count < 1)
         return;
      Table table = (Table)this.Controls[0];
      table.CellSpacing = 0;
     table.CellPadding = 0;
      TableCell cell = table.Rows[0].Cells[1];
     Panel panCur = (Panel)cell.Controls[0].Controls[1];
     Panel panChange = (Panel)panCur.Controls[0];
     HtmlGenericControl ul = (HtmlGenericControl)cell.Controls[0].Controls[0];
      for (int i = 0; i < ul.Controls.Count; i++)
     {
         HtmlGenericControl li = (HtmlGenericControl)ul.Controls[i];
         li.Attributes.Add("onmouseover", "document.getElementById('" + panChange.ClientID + "').style.width='" + 16 * (i + 1) + "px';");
         li.Attributes.Add("onmouseout", "document.getElementById('" + panChange.ClientID + "').style.width='0px';");
         li.Attributes.Add("onclick", Page.ClientScript.GetPostBackClientHyperlink(this, (i + 1).ToString()));
     }
}

可以看到在列表项的点击事件(onclick)中触发了服务器回发,这里是通过Page.ClientScript.GetPostBackClientHyperlink方法完成的,该方法传递两个参数,第一个参数引用引起回发的服务器控件,第二个参数标识回发时传递的参数,这里传递了星形图案的索引。

为什么不在创建列表的同时注册JavaScript脚本,这是由于在CreateChildControls方法执行时,各服务器端控件的ClientID尚未生成,此时注册的脚本在客户端操作时会发生错误。

3.12 为了能够触发事件,首先编写事件参数类StarEventArgs,该类定义了Score属性保存得分:

public class StarEventArgs : EventArgs
{
     public int Score
     {
         get;
         set;
     }
}
3.13 在PostStar中声明事件属性,注意这里和上次任务不同,将事件声明为私有字段并通过属性暴露出来(相比以前的用法更推荐使用这种用法):
private event EventHandler<StarEventArgs> _postScore;
  public event EventHandler<StarEventArgs> PostScore
{
     add
     {
         _postScore += value;
     }
     remove
     {
         _postScore -= value;
     }
}
3.14 编写OnPostScore方法调用事件并从控件层次中取得相应的层以固定得分显示:
private void OnPostScore(object sender, StarEventArgs e)
{
     if (_postScore != null)
         _postScore(sender, e);
      TableCell cell = ((Table)this.Controls[0]).Rows[0].Cells[1];
     Panel panCur = (Panel)cell.Controls[0].Controls[1];
     panCur.Style.Add("width", e.Score * 16 + "px");
     Panel panChange = (Panel)panCur.Controls[0];
     panChange.Style.Add("display", "none");
     HtmlGenericControl ul =
     (HtmlGenericControl)cell.Controls[0].Controls[0];
     ul.Style.Add("display", "none");
}
3.15 实现IPostBackEventHandler接口的RaisePostBackEvent方法,在客户端引起服务器回发后会执行此方法并传递相应的参数,我们只需要在此方法中确保已创建了所有的子控件后引发相应的事件以设置属性即可:
public void RaisePostBackEvent(string args)
{
     if (!string.IsNullOrEmpty(args))
     {
         EnsureChildControls();
          int score = Convert.ToInt32(args);
          StarEventArgs e = new StarEventArgs();
         e.Score = score;
          OnPostScore(this, e);
     }
}

3.16 在Web网站中创建测试页面,注册并声明自定义控件:

<%@ Register TagPrefix="cc" Assembly="ControlLibrary"
     Namespace=?ControlLibrary? %>
<cc:PostStar ID="star" runat="server" Comment="Windows XP"
     OnPostScore="star_PostScore" />
附录:样式表文件:
.stars {width: 80px; height: 16px; text-align: left; overflow: hidden; position: relative; background: url(stars.gif) 0px -32px repeat-x;}
.stars .ulist { list-style: none; position: absolute; bottom:0px; margin:0px; padding: 0; }
.stars .ulist li { display: inline; float: left; width: 16px; height: 16px; cursor: pointer; overflow: hidden; }
.stars .current {width: 0px; height: 16px; background: url(stars.gif) 0px 0px repeat-x;}
.stars .change {width: 0px; height: 16px; background: url(stars.gif) 0px -16px repeat-x;}

4.总结

本次任务里我们创建了一个可以由用户评分的自定义控件,PostStart类继承了IPostEventHandler接口以引发服务器提交,通过引入该接口,我们可以根据需要使任何一个控件引起服务器提交——只需要注册相应的JavaScript事件即可;同时为了防止页面使用多个评分控件时出现错误,实现了INamingContainer接口。在接下来的任务里,将介绍自定义数据绑定控件的开发方法。


ASP.NET自定义控件系列文章

前言

第一天 简单的星级控件 

第二天 带有自定义样式的星级控件

第三天 使用控件状态的星级控件

第四天 折叠面板自定义控件

第五天 可以评分的星级控件

第六天 可以绑定数据源的星级控件

第七天 开发具有丰富特性的列表控件

第八天 显示多个条目星级评分的数据绑定控件

第九天 自定义GridView

第十天 实现分页功能的DataList


全部源码下载

本系列文章PDF版本下载