原文地址:http://imar.spaanjaars.com/QuickDocId.aspx?quickdoc=410
作者:Imar
如何让你的网站的浏览者知道,其所浏览的内容在别的浏览者心中是什么感觉呢?有一个好的主意就是让你的用户评级你网站的内容。
一些站点已经提供了类似的功能。如Amazon使用5星评级法来评级他们所卖的商品。微软的MSDN站点使用9柱图来显示他们技术文章的质量。我自己的站点使用5柱图来让用户评级一篇文章(在左手边的列里)(译者注:详看原文左侧)。因为要为站点增加评级的功能,所以可以把这个功能封装成一个自定义的服务器控件。这篇文章将告诉你如何开发这样一个控件。
必备条件
这篇文章假定你的开发环境为c#和asp.net2.0,并且要有一些控件开发的知识。但是,即使你对控件开发不是特别的了解,也可以同过编写少量的代码直接在你的页面使用本文所讲述的这个控件。
介绍
我们将在这篇文章里开发并使用这个控件(被命名为ContentRating),它将允许用户对某项内容进行评级。其用一定数量的星星来显示评级结果,如下图所示:
图1 - ContentRating控件
这个控件不提供检索和存储评级值的功能,而这个功能实际上是经常要用的。你可以在你自己的逻辑中解决这个问题。
在我们开始开发这个控件之前,我做了一个简短的功能列表。这些功能已经在这个控件中实现了
·必须使用图片(如星星图)来指定某个内容的质量
·必须能显示任意数量的星星图片。一些站点可能需要5颗星,但你也可以修改星星的数量,比如所10颗星。
·必须能触发一个事件指明用户所评级的那个内容。也必须能触发一个评级完成后的事件。
·必须易于使用,不要有太多的配置项。所以,应该有默认的星星图片。
·必须能够保存用户已经评级过的内容的唯一ID,以使用户不能对相同内容做重复评级。
实现这个要求列表里的功能,你将会从中学到很多让你感到有兴趣的知识。在控件里嵌入图片资源是非常实用的,保存用户所评级的内容的唯一ID也是一项有趣的挑战。接下来我将告诉你如何完成这些任务。
控件设计
让我们先来看看封装在这个ContentRating控件里的非常重要的几个类。首先是“ContentRating”类。这个类继承自CompositeControl这是asp.net2.0里的一个控件基类,它使得开发包含子控件的控件变得非常容易。我选择这个类是因为ContentRating控件里包含可以回发的服务端的图片(星星图),从CompositeControl继承,你将会得到一些原来必须自己开发的功能。这些功能之一就是CompositeControl实现了INamingContainer接口,这将确保所有子控件自动得到在整个应用程序内唯一的ID。下面的图就是ContentRating类的类图。
图2 - ContentRating类的类图
这个类有7个属性,7个方法和2个事件,以下将对其每一个属性、方法、事件做分别说明。
属性
属性名称 | 说明 |
DataSource | 保存各评级项评级数的整形数组。如:{2, 0, 0, 5, 5},意思是2个用户评级为1星,分别有5个用户评级为4星和5星 |
HalfStarImage | 显示一个一半的星星的路径。当平均值在x.3和x.7之间时使用。比如评级平均值为3.56,就是3个全星图,1个半星图,1个无效星星图 |
ItemId | 保存所评级内容的唯一ID,可以使用整形或GUID类型,等等 |
LegendText | 显示在星星下面的文本。(译者注:两个参数{0}-投票数;{1}-平均值) |
OneStarImage | 全星图路径 |
OneStarImageDisabled | 无效星星图路径 |
TagKey | CompositeControl默认用<span>标签呈现,但我重写了TagKey属性,使之用<table>标签呈现 |
除了这些属性,ContentRating类还有一些方法
方法
方法名称 | 说明 |
CreateChildControls | override基类的CreateChildControls方法。调用私有方法CreateControlHierarchy来构造子控件 |
CreateControlHierarchy | 一个用来构造子控件的私有方法。它将把星星图片加到HTML的table标记里 |
DataBind | override基类的DataBind方法。在ViewState里保存DataSource。调用私有方法CreateControlHierarchy来构造子控件 |
GetImageUrl | 一个私有方法,返回图片的url路径。如果在公共属性中设置它,将返回请求客户端可用的URL。否则将使用Page.ClientScript.GetWebResourceUrl指向一个嵌入的图片 |
OnBubbleEvent | override基类的OnBubbleEvent方法。星星图触发一个事件执行这个方法。在这个方法中,控件激发其自身的事件,事件执行完毕后返回true |
OnRated | 完成评级后被OnBubbleEvent调用 |
OnRating | 评级某个内容时被OnBubbleEvent调用 |
这个控件也暴露了两个事件给页面开发者
事件
事件名称 | 说明 |
Rating | 当用户单击某个星星图片的时候触发该事件,此时Rated事件并没有被调用。这样则允许页面开发人员检测RateEventArgs对象,以决定是否取消Rated事件(比如,如果用户已经对此内容评级过了则取消Rated事件) |
Rated | 当用户对某内容评级完后激发 |
我们稍候将在“内部工作方式”一节看到这些属性、方法和事件。
控件内另一个比较重要的类就是RateEventArgs类。这个类的实体通过页面的Rating和Rated事件传递。页面开发人员检测这个类来决定是否取消Rated事件,比如当用户已经对此内容评级过的时候。在Rated事件中,这个类提供了一个整形的评级值。页面开发人员可以在更新数据库或者重新绑定的时候用到RateValue这个属性。下面是RateEventArgs类的类图:
图3 - RateEventArgs类的类图
取消属性允许页面开发人员取消Rated事件。HasRated属性决定当前用户是否已经对当前内容评级过了。ContentRating控件通过cookie保存用户的评级情况,所以如果cookie中记录了相关的值,这个属性就是true。在Rating事件里,当HasReted的值为true的时候你设置取消属性,以使一个用户不能对同一内容评级两次。
RateValue实际为一个用户对某个内容的评级值,这个属性是整形,并且其值介于数据源数组的最大值和最小值之间。
最后,SupportsCookies属性在Rating事件里非常有用。当这个属性的值为false,意味着当前用户的客户端不支持cookie,所以ContentRating控件不能保存用户是否已评级过某个内容的信息。这个情况下,你可以在Rating事件里用一些其他方法来判断该用户是否已经评级过某个内容,比如检索数据库之类的。
最后我们来看看RatingSettings类。这个类用来计算平均值,总评级值,决定是否显示“半星图”等。
图4 - RatingSettings类的类图
要使用这个类,你需要在构造函数中提供一个各分值评级数的整形数组。这样,这个类就可以计算评级平均值,总评级数,是否显示“半星图”等。让我们来看一个例子来了解这个类是如何工作的。
假设你的评级数组是这样的:
ContentRating1.DataSource = new int[] {2, 0, 0, 5, 5};
它的意思是两名用户评级为1分,分别有5名用户评级为4分和5分。总共评级数为12(TotalRates),评级平均值为3.91((2*1 + 5*4 + 5*5) / 12)。在这个例子中,控件将显示4个全图和1个无效图。评级平均值为3.91,总投票数12,TruncatedValue值为4,不显示“半星图”。其图例如下:
让我们在来看看另一个例子:
ContentRating1.DataSource = new int[] {2, 0, 0, 5, 13};
在这个例子里,评级平均值为4.35,总评级数为20,TruncatedValue为4。因为,平均值的小数部分介于0.3和0.7之间,所以要显示“半星图”。控件将显示4个全星和1个半星。其图例如下:
如果你想知道是这些如何计算的,可以看一看RatingSettings类里的构造函数。在本文结尾你可以下载ContentRating控件的全部源码。
ContentRating控件的内部工作方式
现在你已经了解了ContentRating控件的一些重要的类,接下来我们将看看它们的代码。我不想出示所有的代码,而只是讲一些重要的概念。记住,你可以在本文结束下载全部源代码。
我们从看类的声明开始:
[
DefaultProperty("OneStarImage"),
DefaultEvent("Rate"),
ToolboxData("<{0}:ContentRating runat='server' />"),
ToolboxBitmap(typeof(ContentRating), "Resources.ContentRating.bmp")
]
public class ContentRating : CompositeControl
{
}
前文我们说过这个控件继承自CompositeControl,这是一个包含子控件的非常实用的基类。我也给ContentRating类增加了一些元数据。ToolboxData决定控件在aspx页面上的标记,站位符{0}将被定义在AssemblyInfo.cs里的TagPrefix所替换。
[assembly: TagPrefix("Spaanjaars.Toolkit", "isp")]
当你从工具箱拖拽控件到你的页面上,其显示的标记为:
<isp:ContentRating ID="ContentRating1" runat="server" />
另一个有趣的元数据是ToolboxBitmap,它告诉编译器在Visual Studio里的工具箱内该控件所显示的图片。为了实现这样的功能,在你编译的时候需要导入内嵌资源(更多的相关内容将在稍后讲)。请注意,因为图片文件都存放在了Resources文件夹内,所以图片的名字前要加上Resources这个前缀。否则将不能找到图片。
接下来是私有变量的声明,这些私有变量贯穿整个类。而后,所有的私有变量都有其对应的公共属性。
私有变量下面的区域被称作“Events”。这个代码区包含两个事件:Rating和Rated,分别在用户评级某个内容之前和之后被激发。这个两个事件使用标准的c#事件处理代码,使用add和remove来操作事件列表。更多的关于事件处理的语法,你可以参看一本书“Professional ASP.NET 2.0 Server Control and Component Development”,在本文稍后的“参考资源”一节中有着本书的链接。
实际上这两个事件实在OnBubbleEvent方法里触发的,而该方法是在image子控件被单击后被调用的。在我们讨论完其它的代码之后再来讲解这个方法。
公共属性包含图2所示的7个属性。除了this.EnsureChildControls用来确保子控件已经被建立之外,其他的属性都是很常用的写法。这些属性是非常有用的,你可以在使用这个控件的时候修改这些属性,并且所做的改变都回有所体现。比如,当你修改了OneStarImage属性后,设计窗口会立刻显示你所选择的那个新图片。
在接下来的“Databinding and Control Creation”代码区包含了数据绑定方法以及创建控件的层次结构。让我们从DataBind方法开始:
public override void DataBind()
这个方法首先判断你是否设置了ItemId,如果没有ItemId将会抛出一条错误信息。注意即使没有DataSource也是可以的。这是因为当没有任何人评级的时候应该要显示一个全空的评级控件。如此,DataSource就要被设置成默认的5个零的数组(译者注:这样做不会实现前文所提到的可以设置任意颗星星的功能)。
{
if (ItemId == null)
{
throw new ArgumentException(@"Can't bind a datasource
without a valid ItemId");
}
if (dataSource == null)
{
dataSource = new int[] { 0, 0, 0, 0, 0 };
}
base.DataBind();
Controls.Clear();
ClearChildViewState();
TrackViewState();
ViewState["RateItems"] = dataSource;
CreateControlHierarchy(dataSource);
ChildControlsCreated = true;
}
然后,这段代码调用了基类的DataBind方法,所以DataBinding事件才能被触发。接着清除全部子控件,把DataSource保存到ViewState里,调用创建子控件的CreateControlHierarchy方法。该方法接收一个DataSource参数来创建正确的子控件。
在CreateChildControls方法中也执行者相似的任务
protected override void CreateChildControls()
{
Controls.Clear();
if (ViewState["RateItems"] != null)
{
int[] tempDataSource = (int[])ViewState["RateItems"];
CreateControlHierarchy(tempDataSource);
}
else
{
if (this.DesignMode)
{
int[] tempDataSource = new int[] { 18, 23, 17, 12, 45 };
CreateControlHierarchy(tempDataSource);
}
}
}
这段代码首先检查数据源(RateItems)是否已经存到ViewState内。如果控件产生了回发事件则会执行这个条件。这都依赖于页面开发人员,如果不绑定数据就回发到服务端,控件也可以从ViewState中取得数据源。当然页面必须要开启ViewState功能。
如果没有ViewState,并且控件是设计模式(Visual Studio里aspx页的设计模式),这段代码将会虚构一个整形数组。
以上两种情形都会调用CreateControlHierarchy方法。
这个方法首先通过构造函数,参数为DataSource,实例化一个RatingSettings类
RatingSettings mySetings = new RatingSettings(theDataSource);
早先时候我解释过,这将计算出RatingSettings类的一些诸如评级平均值之类的公共属性(只读)。
然后创建一个TableRow对象,并根据数据源循环操作。在这个循环内,新的TableCell和ImageButton将被创建。ImageButton设置CommandArgument和CommandName属性,以便知道评级值属于那个分值。
starImage.CommandArgument = numberOfCells.ToString();
starImage.CommandName = "Rate";
numberOfCells变量从1循环到数据源数组的长度。所以你的CommandArgument属性将是1,2,3之类的。
接下来的代码决定图片的显示。记住,这里有3种图片:全图,半图和无效图。
if (numberOfCells <= mySetings.TruncatedValue)
当你numberOfCells少于或者等于TruncatedValue的时候,则显示全图。所以当评级平均值为3.6的时候,TruncatedValue为3,将添加3个全图。接下来如果应该添加半图则添加,这取决于ShowHalfImage属性,而且numberOfCells要等于TruncatedValue + 1。当评级平均值为3.6的时候,TruncatedValue为3并且ShowHalfImage为true,所以第4张图是一个半图。
{
imageUrl = GetImageUrl(oneStarImage, Constants.RatingOneStarImage);
}
else
{
if (numberOfCells == mySetings.TruncatedValue + 1
&& mySetings.ShowHalfImage)
{
imageUrl = GetImageUrl(halfStarImage, Constants.RatingHalfStarImage);
}
else
{
imageUrl = GetImageUrl(oneStarImageDisabled,
Constants.RatingOneStarDisabledImage);
}
}
starImage.ImageUrl = imageUrl;
最后剩余的图全都是无效图。
每次得到图片路径的时候都使用了GetImageUrl方法,此方法定义如下:
private string GetImageUrl(string imageUrl, string embeddedImage)
当图片路径为虚拟路径的时候将调用基类的ResolveUrl方法,转换为客户端可用的url路径。但是如果没有设置图片属性,则将使用程序集内嵌入的默认图片。为了便于管理,我使用了常量(定义在Constants.cs文件里)来指向这些内嵌图片。如下:
{
string localImageUrl;
if (String.IsNullOrEmpty(imageUrl))
{
localImageUrl = this.Page.ClientScript.GetWebResourceUrl
(this.GetType(), embeddedImage);
}
else
{
localImageUrl = base.ResolveUrl(imageUrl);
}
return localImageUrl;
}
internal static class Constants
用这种方法你不必再记住图片的全部路径,但你需要通过图片的名字来指向这些图片。
{
internal const string RatingOneStarImage =
"Spaanjaars.Toolkit.Resources.OneStar.gif";
internal const string RatingOneStarDisabledImage =
"Spaanjaars.Toolkit.Resources.OneStarDisabled.gif";
internal const string RatingHalfStarImage =
"Spaanjaars.Toolkit.Resources.HalfStar.gif";
}
用Page.ClientScript.GetWebResourceUrl方法来取得内嵌的图片,你需要做两件事情。
1、在图片的属性中设置生成操作为嵌入的资源。
图5 - 在解决方案管理器的属性面板里设置图片为嵌入资源
2、告诉编译器你想要包含的嵌入式图片,你需要在AssemblyInfo.cs里写下如下信息。
[assembly:WebResource(Constants.RatingHalfStarImage,"image/gif")]
[assembly:WebResource(Constants.RatingOneStarDisabledImage,"image/gif")]
[assembly:WebResource(Constants.RatingOneStarImage,"image/gif")]
注意,我是调用相关的常量来指向图片的。如果没有这些网页资源指示,你将会收到一个编译器错误提示,并且你的图片将无法被请求到。
在CreateControlHierarchy方法循环的结尾附近,ImageButton被添加到了TableCell中,这个TableCell被添加到TableRow里。该方法的最后,整个TableRow被添加到了控件的控件集合中。最终,控件的ChildControlsCreated属性被设置成true,说明该控件的子控件已经创建完毕。
最后我们再来看看OnBubbleEvent这个方法,当子控件(ImageButton控件)发生单击事件时会触发该方法。如果触发该事件的是一个命令并且命令名称为Rate则执行其内部代码。如果不是这种情况,那么handled被设置成false并退出该方法。如果ImageButton触发了该方法,那么其执行代码为:
RateEventArgs myArgs = new RateEventArgs(Convert.ToInt32(ce.CommandArgument), true);
HttpCookie rateCookie;
cookieKey = String.Empty;
cookieValue = String.Empty;
if (Context.Request.Browser.Cookies == true)
{
myArgs.SupportsCookies = true;
cookieKey = "Rate_" + ItemId.ToString();
cookieValue = ce.CommandArgument.ToString();
rateCookie = Context.Request.Cookies["Rate"];
if (rateCookie == null)
{
myArgs.HasRated = false;
}
else
{
if (rateCookie.Values[cookieKey] == null)
{
myArgs.HasRated = false;
}
else
{
myArgs.HasRated = true;
}
}
}
else
{
myArgs.SupportsCookies = false;
}
这段代码用来检查cookie,如果没找到相关cookie则意味着用户之前没有评级过该内容。如果有cookie则以目前ItemId为关键字继续检查,当发现了名为Rate_ItemId的cookie时则说明该用户已经评级过该内容了。HasRated被设置成true还是false就是依靠着客户端的cookie功能。
接下来OnBubbleEvent方法还要触发一个事件
OnRating(myArgs);
一旦OnRating被调用,则aspx页的事件处理代码预定的OnRating事件将运行。作为事件处理的一个参数,代码将获得RateEventArgs类的实体,通过这个实体来让页面开发人员决定是执行Rated事件还是取消Rated事件。相关代码如下:
protected void ContentRating1_Rating(object sender, Spaanjaars.Toolkit.RateEventArgs e)
{
if (e.HasRated)
{
e.Cancel = true;
}
}
如果取消属性被设置成true,那么Rated事件将不会被激发。这样,你的用户就不能给相同的内容平价两次。如果你不介意一个用户多次评级同一内容的话,那你可以根本不用处理Rating事件。
如果取消属性不是true的话,则运行如下代码:
if (!myArgs.Cancel)
{
// Create a new cookie value for this rate.
rateCookie = new HttpCookie("Rate");
rateCookie.Expires = DateTime.Now.AddMonths(6);
rateCookie.Values.Add(cookieKey, cookieValue);
Context.Response.Cookies.Add(rateCookie);
// Raise the Rated event to notify clients.
OnRated(myArgs);
handled = true;
}
最后Rated事件被激发,你可以在页面中处理这个事件。参数的RateValue属性就是用户所选的评级值。你可以通过这个属性来更新你的数据源。例如:
protected void ContentRating1_Rated(object sender, Spaanjaars.Toolkit.RateEventArgs e)
{
int rateValue = e.RateValue;
Guid itemId = myContentItem.Id;
Content.InsertRating(itemId, rateValue);
}
注意我写的这些代码,ContentRating控件不知道任何有关数据源的信息,也不知道用户所选的评级值,也不能存储用户所选的评级值。因为这些是页面开发人员的工作。
现在就结束了ContentRating类的设计和实现。虽然它自身不能从数据库中检索和存储评级值,但是在下一部分你将看到它作为一个控件在页中被使用将是非常简单的,而且你也将知道一个通过写代码完成从数据库中检索和存储评级值功能的好办法。
使用ContentRating控件
我们可以直接使用ContentRating控件,接下来说一下步骤。
1、在Visual Studio 2005编译这个控件。要确定是release出来的
2、在Visual Studio 2005新建一个web应用程序
3、打开Default.aspx页,切换到设计视图。右键单击工具箱(找不到工具箱就按Ctrl+Alt+X)然后单击选择项,找到刚才编译好的
4、从工具箱拖拽这个控件到你的页上,你将看到这个控件有5颗星星(3个全图、1个半图和1个无效图)
5、创建Rating事件和Rated事件
6、在Page_Load方法内增加ContentRating控件的初始化代码,你将在这里设置数据源,要求数据是一个各分值评级数的整形数组。
7、调用DataBind方法把数据源绑定到你的控件
接下来的代码向你出示了使用ContentRating控件时的页面代码。这个例子的目的在于通过使用ViewState进行回发存储,来了解如何使用数据库去检索和存储评级值。
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ContentRating1.ItemId = Guid.NewGuid();
ContentRating1.DataSource = Values;
ContentRating1.DataBind();
}
}
protected void ContentRating1_Rating(object sender,
Spaanjaars.Toolkit.RateEventArgs e)
{
if (e.HasRated)
{
e.Cancel = true;
}
}
protected void ContentRating1_Rated(object sender,
Spaanjaars.Toolkit.RateEventArgs e)
{
int[] tempValues;
tempValues = Values;
tempValues[Convert.ToInt32(e.RateValue) - 1] += 1;
Values = tempValues;
ContentRating1.DataSource = Values;
ContentRating1.DataBind();
}
public int[] Values
{
get
{
object values = ViewState["Values"];
if (values != null)
{
return (int[])values;
}
else
{
return new int[] { 17, 0, 0, 5, 5 };
}
}
set
{
ViewState["Values"] = value;
}
}
页面上的标记为:
<isp:ContentRating
ID="ContentRating1"
runat="server"
OnRated="ContentRating1_Rated"
OnRating="ContentRating1_Rating"
LegendText="{0} rates / {1} avg."
/>
在你的浏览器中,控件将如下显示:
图6 - ContentRating控件在浏览器中的显示(仅执行了Page_Load方法)
如果你单击了第5颗星,控件的显示将变为:
图7 - ContentRating控件在浏览器中的显示(单击第5颗星后回发服务端)
如果你再次单击某个星星,则不会发生任何变化,因为你已经评级过这个内容了。
总结
开发控件是一项令人头疼的工作。但是,当你做了一遍之后,你会感觉这是非常简单的。一旦你学会了自定义控件的开发,它们将变成你解决方案里常常用到的一部分。把一些常用的功能封装到自定义控件里,你就可以通过简单的拖拽方便快捷的实现那些功能。
希望这篇文章能够唤起你开发控件的热情。如果你对这个控件有什么建议或者改进的要求,又或者通过我的控件而开始开发你自己的控件,请让我知道。
改进和扩展
这是一个非常基础的控件,我能想到对它的一些改进和扩展包括:
·使用CompositeDataBoundControl代替CompositeControl。CompositeDataBoundControl包括一些数据绑定控件的附加行为。但是我研究的不够深入,所以没能使用它。
·扩展每个星星图片的OnMouseOver行为,当你鼠标经过时,星星以高亮显示。这样,用户可以很方便的看到他所评级的分数是多少。你应该写一些JavaScript使鼠标经过星星时改变图片。你可以嵌入这段JavaScript到你的程序集,就像嵌入星星图片那样。
·给ContentRating增加一个TextBox控件,使用户能够写一些为何如此评级的原因
·让页面开发人员能够设置图片的一些样式,比如颜色,尺寸等。这样页面开发人员能够通过下拉列表快速的修改图片的样式。当然你需要把这些图片嵌入到程序集中并且要保证GetImageUrl能够得到正确的图片路径。
我是在写这篇文章的同时写ContentRating这个控件的,如果要在现实环境中使用还需要做一定的测试。如果在你的使用过程中,该控件出现了什么问题,请告诉我,我将改正它们并且更新此文
本文作者:未知