B.3 实现控件
DateChooser的代码量比较大,无法把所有代码都置于书中,只能将其中一些关键代码加以讲解,完整的控件项目请参看本书附带资源。
B.3.1 实现客户端行为
DateChooser的客户端逻辑主要集中在DateChooser.js脚本文件中。在这个文件中,我们实现了一个组件化的脚本类——DateChooser。它的构造函数最多接受7个参数,分别代表DateChooser控件对应的元素ID,DateChooser实例ID,日期格式,可选日期范围,显示、隐藏选择面板时的特效。

注意 在实现日期选择这一核心功能后,DateChooser.js的代码不到150行,当实现了所有附加的功能后,它的代码超过了400行,可见“细节决定成败”是说起来容易做起来难。
通过调用DateChooser实例的initiate()方法将初始化控件的行为。初始化过程中将查找并保存控件关键部件的指针,比如输入框。然后初始化各按钮(对应为A元素)的行为。并初始化一些鼠标的交互效果。此外,我们利用JQuery的链式风格使代码简洁很多,比如:
$(".calendarPrev > img",this._element)
.mouseover(function(){this.src = DateChooser.prevHotSrc;})
.mouseout(function(){this.src = DateChooser.prevSrc;})
.mousedown(function(){this.src = DateChooser.prevDownSrc;})
.mouseup(function(){this.src = DateChooser.prevHotSrc;});
以上一句代码为Class等于calendarPrev的元素内的IMG元素指定了mouseover,mouseout, mousedown,mouseup四个事件的响应程序。
接下来,初始化函数填充年视图,即Class为calendarYearTable的表格的内容。
控件中各交互元素的事件响应程序也在此时指定。值得指出的是,此处我们使用了Ajax.js中定义的Function.createDelegate()方法创建委托对象,再把委托对象绑定到控件事件上。这一复杂过程带来的好处是,事件触发时,响应程序仍然可得到你想来的上下文环境,即this指针,要知道默认情况事件响应程序得到的this指针将指向事件源,而不是你想要的DateChooser类的实例。
this._bodyClickHandler =
Function.createDelegate(this,this._bodyOnClick);
$addHandler(document.body,'click',this._bodyClickHandler);
不借助于Function.createDelegate()创建委托,我们也可以通过闭包来控制事件触发时的this指针。思考以下代码:
<body>
<div id="divButton">I am a button!</div>
<script type="text/javascript">
function Button(id,msg){
this._id = id;
this._msg =msg;
this._element = document.getElementById(this._id);
}
Button.prototype.initiate = function(){
this._element.onclick = this.CreateClickHandler();
}
Button.prototype.CreateClickHandler = function(){
var context = this;
return function(){
context.onClick.apply(context,arguments);
}
}
Button.prototype.onClick = function(){
this._element.innerText += this._msg;
}
new Button('divButton','Hello world!').initiate();
</script>
</body>
也许您会对this._element.onclick = this.createClickHandler()这行代码心存疑惑,通常我们会把一个函数指针赋值给事件,也就是说应该是this._element.onclick = this.createClickHanlder更符合常理一点。不过此处我们执行的createClickHandler()函数其实返回的还是一个函数,只是这个函数用Javascript闭包技术加了一个封装,这样最终执行的onClick()函数能获得指向Button实例的this指针,从而实现和C#等现代语言类似的逻辑。
在显示和隐藏DateChooser的日期选择面板时,我们使用了“单例”模式,也就是说,一个页面中同时弹出的选择面板最多只能一个。为了实现这个效果,我们用DateChooser._shownChooser这一“静态”变量来保存当前显示的选择面板的指针。
DateChooser.prototype.showPanel = function(){
//关闭其它正在显示的日期面板
if(DateChooser._shownChooser && DateChooser._shownChooser != this){
DateChooser._shownChooser.hidePanel();
}
DateChooser._shownChooser = this;
var location = Sys.UI.DomElement.getLocation(
$('.calendarDropDownButton',this._element).get(0));
location.y += Sys.UI.DomElement.getBounds(this._input).height;
Sys.UI.DomElement.setLocation(
$('.calendarView',this._element).get(0),location.x,location.y);
//最后弹的日期面板在最上面
DateChooser._zIndex ++;
$('.calendarView',this._element).css('zIndex',DateChooser._zIndex);
eval("$('.calendarView',this._element)."+this._fxShow);
}
DateChooser.prototype.hidePanel = function(){
eval("$('.calendarView',this._element)."+this._fxHide);
DateChooser._shownChooser = null;
}
DateChooser.prototype._bodyOnClick = function(e){
if(DateChooser._shownChooser){
DateChooser._shownChooser.hidePanel();
DateChooser._shownChooser = null;
}
}
在用户输入日期时,要判断输入的正确性,如果输入有误,要给予提示,并自动更正回原来正确时的状态。在实现自动更正时,需要实现一定的延时效果,因为如果用户在输入的同时就进行更正的话,会干扰输入操作。比如最小可选日期为2007-2-1,用户想输入的是2007-12-31,在他输入12月的2之前,输入框的值是2007-1-31,如果此时就对输入值进行修正的话,就会使得用户输入无法进行下去。所以,对输入值的修正操作应该在用户停止输入之后进行。
DateChooser.prototype._inputOnKeyUp = function(){
var date = this._validate(this._input.value);
if(this._correctError){
window.clearTimeout(this._correctError);
this._correctError = null;
}
var obj = this;
if(date){
$(this._input).removeClass('calendarError');
this._correctError = window.setTimeout(function(){
obj._input.value = date.localeFormat(obj._dateFormat);
obj._synchronizeFromInput();
if(obj._input.onchange){
$(obj._input).change();
}
},1000);
}
else{
$(this._input).addClass('calendarError');
this._correctError = window.setTimeout(function(){
obj._synchronizeToInput();
$(obj._input).removeClass('calendarError');
},1200);
}
}
日期选择面板有两种界面,分别为月视图和年视图。月视图一次显示一个月的日期,年视图显示十二个月。在填充月视图的内容时,比较麻烦的地方是取得当前要显示的日期数组。因为很多时候,某月的第一天并不是周日,最后一天也不是周六,所以不能简单地从该月1号呈现到31号,而需要求出选择面板中首尾显示的其他月份的日期。我们提供了一个MonthCalendar内部类专门提供此功能:
DateChooser.prototype.MonthCalendar = function (year,month){
var firstDay = new Date(year,month-1,1);
var lastDay = new Date(year,month,1);
var dayOfFirstDay = firstDay.getDay();
firstDay = new Date(firstDay * 1 - (DateChooser.TICKETOFDAY * dayOfFirstDay));
var dayOfMonth = new Array();
for(var i = 0 ; firstDay < lastDay || i % 7;i++){
dayOfMonth.push(firstDay);
firstDay = new Date(firstDay * 1 + DateChooser.TICKETOFDAY );
}
return dayOfMonth;
}
客户端代码我们就分析到这里,更多内容请查看本书附带资源中的代码文件,关键的代码都配有详尽的注释。
此外需要说明的是,由于控件使用了几个使用了alpha透明的png图片,而这样的图片在老版本的IE中存在显示Bug,也就是说透明效果不能正确显示,为了修正这个Bug,我们使用了iepngfix.htc提供的行为。Htc仅被IE支持,好在此处它只需为IE提供服务:
<!--[if lt IE 7]>
<style type="text/css">
.calendar img { behavior: url(iepngfix.htc) }
</style>
<![endif]-->
<!--[if lt IE 7]>…<![endif]-->这样的语句只能被IE理解,其他浏览器会忽略这些语句。类似的Hack技巧,从原则上讲,我们是应该尽量避免使用的。
由于以前很多B/S架构开发者对客户端脚本抱有一种轻视的态度,造成现在很多开发者不能全面地把握B/S开发的要领。因此,我在这里花了一些篇幅讨论脚本代码,为的是以实例的方式加深大家对脚本开发的认识。
B.3.2 控件呈现
准备好了客户端功能后,接下来要做的就是组织它们。
首先把控件所需的客户端文件在AssemblyInfo.cs中声明为WebResource:
[assembly: WebResource("DateChooserLibrary.CSS.DateChooser.css", "text/css; charset=UTF-8")]
[assembly: WebResource("DateChooserLibrary.Images.bNext.png","image/png")]
//… …
[assembly:WebResource("DateChooserLibrary.Javascript.DateChooser.js","application /x-javascript;charset=UTF-8",PerformSubstitution=true)]
//… …
[assembly: WebResource("DateChooserLibrary.Javascript.iepngfix.htc", "text/x- component")]
在声明DateChooser.js时,需要使用PerformSubstitution=true参数,因为该文件中有一些需要替换成资源路径的表达式,比如:
DateChooser.monthSrc =
'<%= WebResource("DateChooserLibrary.Images.month.png") %>';
DateChooser.prevHotSrc =
'<%= WebResource("DateChooserLibrary.Images.bPrev-hot.png") %>';
此外,控件还需要一批HTML标签来组织它的结构,这些HTML标签是DateChooser.js脚本操作的对象,没有这些标签,客户端代码也就“巧妇难为无米之炊”了。而且,在设计时我们也需要提供一些HTML代码支持“所见即所得”的功能。
运行时呈现的HTML代码类似如下:
<div id="calendar1" class="calendar">
<div class='calendarInputView'>
<a class='calendarDropDownButton'>
<img /></a>
<input class='calendarInput' value='' id='calendar1_input'
name='calendar1$input' />
<a class='calendarAddButton'>
<img /></a> <a class='calendarReduceButton'>
<img /></a>
</div>
<div id="calendar1_panel" class='calendarView'>
<div class="calendarMonthView">
<table class="calendarMonthTable" cellpadding='0' cellspacing='0'>
<thead class="calendarMonthHearder">
<th class='calendarSunday'>
</th>
<th class='calendarDay'>
</th>
<th class='calendarDay'>
</th>
<th class='calendarDay'>
</th>
<th class='calendarDay'>
</th>
<th class='calendarDay'>
</th>
<th class='calendarSaturday'>
</th>
</thead>
<tbody class="calendarMonthDay">
</tbody>
</table>
</div>
<div class="calendarYearView">
<table class="calendarYearTable" cellpadding="0" cellspacing="0">
<tbody>
</tbody>
</table>
</div>
<div class="calendarNavigator">
<a class="calendarPrev">
<img src="Images/bPrev.png" />
</a><a class="calendarNext">
<img src="Images/bNext.png" />
</a><a class="calendarTitle"></a>
</div>
</div>
</div>
可以看到这些代码只是一个框架,只有在经过DateChooser.js脚本的加工后,才能呈现最终的用户界面。在呈现设计时效果时,我们可不能直接把这个“空壳”呈现给页面设计者,所以我们必须为运行时和设计时呈现大部分相同,细节却尽相同的两套HTML代码。
为此,DateChooser类将控件的呈现过程分解成很多子程序,分别负责呈现不同的区块,在这些子程序中,我们多次利用DesignMode属性判断控件当前处于运行时还是设计时,如图B-18所示。比如负责呈现输入UI(即calendarInputView)的RenderBeginTag()方法:
public override void RenderBeginTag(HtmlTextWriter writer)
{
writer.RenderBeginTag(TagKey);
writer.AddAttribute(HtmlTextWriterAttribute.Class,
"calendarInputView");
writer.RenderBeginTag(HtmlTextWriterTag.Div);
writer.AddAttribute(HtmlTextWriterAttribute.Class,
"calendarDropDownButton");
writer.RenderBeginTag(HtmlTextWriterTag.A);
if (DesignMode)
{
writer.AddAttribute(HtmlTextWriterAttribute.Src,
Page.ClientScript.GetWebResourceUrl(typeof(DateChooser),
"DateChooserLibrary.Images.edit_0.gif"));
}
writer.RenderBeginTag(HtmlTextWriterTag.Img);
writer.RenderEndTag();
writer.RenderEndTag();
RenderChildren(writer);
writer.AddAttribute(HtmlTextWriterAttribute.Class,
"calendarAddButton");
writer.RenderBeginTag(HtmlTextWriterTag.A);
if (DesignMode)
{
writer.AddAttribute(HtmlTextWriterAttribute.Src,
Page.ClientScript.GetWebResourceUrl(typeof(DateChooser),
"DateChooserLibrary.Images.edit_1.gif"));
}
writer.RenderBeginTag(HtmlTextWriterTag.Img);
writer.RenderEndTag();
writer.RenderEndTag();
writer.AddAttribute(HtmlTextWriterAttribute.Class,
"calendarReduceButton");
writer.RenderBeginTag(HtmlTextWriterTag.A);
if (DesignMode)
{
writer.AddAttribute(HtmlTextWriterAttribute.Src,
Page.ClientScript.GetWebResourceUrl(typeof(DateChooser),
"DateChooserLibrary.Images.edit_2.gif"));
}
writer.RenderBeginTag(HtmlTextWriterTag.Img);
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderEndTag();
}

图B-18 DateChooser控件的呈现过程被分解成众多子程序
化整为零的好处在于可以方便地组织呈现行为,比如用户在设计时智能任务列表中选择显示选择面板,则设计器的GetDesignTimeHtml()方法调用控件的RenderContents()方法,否则只调用RenderBeginTag()方法和RenderEndTag()方法。
从代码可读性的角度来讲,当某个逻辑需要很长的代码来完成时,就要考虑用多个子程序来组织它们。如果每个子程序都有能清楚表达其功能的函数名,那么由这些清晰的子程序名组成的父函数就能清楚地表达它的执行过程。
public override void RenderControl(HtmlTextWriter writer)
{
if (_supportJS)
{
AddAttributesToRender(writer);
RenderBeginTag(writer);
RenderContents(writer);
RenderEndTag(writer);
}
else
{
RenderChildren(writer);
}
}
在组织客户端文件时,我们用Page.ClientScript注册脚本文件,通过往Page.Header中添加HtmlLink对象注册CSS样式表文件,Literal则用来呈现修补IE显示Png文件的Bug的样式代码。
B.3.3 设计时支持
DateChooser拥有丰富的设计时支持。虽然这些功能需要大量的类来实现,但实现起来并没有想像中的那么难。
DateChooser控件添加的属性中,有多个都为DateTime类型,.NET Framework 中已为DateTime类型实现了优秀的编辑器,我们只要应用一下即可,如图B-19所示

图B-19 DateTimeEdtior
[Editor(typeof(DateTimeEditor),typeof(UITypeEditor))]
[Category("Behavior")]
[TypeConverter(typeof(DateConverter))]
[Description("选中日期")]
public DateTime SelectedDate
{
//… …
}
不过DateTime类型默认的类型转换器DateTimeConverter会把DateTime类型数据转成带时间部分的字符串,这不是我们想要的,所以我们实现了DateCoverter类。
public class DateConverter:DateTimeConverter
{
public override object ConvertTo(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture,
object value, Type destinationType)
{
if ((destinationType == typeof(string)) && (value is DateTime))
{
string format;
DateTime time = (DateTime)value;
if (culture == null)
{
culture = CultureInfo.CurrentCulture;
}
DateTimeFormatInfo info = (DateTimeFormatInfo)culture
.GetFormat(typeof(DateTimeFormatInfo));
if (culture == CultureInfo.InvariantCulture)
{
return time.ToString("yyyy-MM-dd", culture);
}
format = info.ShortDatePattern;
return time.ToString(format, CultureInfo.CurrentCulture);
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
值应为虚拟路径的CssFilePath属性所需的编辑器可从UrlEditor类继承:
public class CssUrlEditor:System.Web.UI.Design.UrlEditor
{
protected override string Caption
{
get
{
return "选择Css";
}
}
protected override string Filter
{
get
{
return "CSS文件(*.css)|*.css|所有文件(*.*)|*.*";
}
}
}
通过应用UrlPropertyAttribute,可以让CssFilePath属性在源代码视图也能得到编辑器的支持。
[UrlProperty("*.css")]
[Category("Behavior")]
[DefaultValue(CssFilePathConverter.EmbeddedCss)]
[Editor(typeof(CssUrlEditor),typeof(UITypeEditor))]
public string CssFilePath
{
//… …
}
其他几个属性都为字符串类型,只需通过TypeConverter提供一些标准值以供选择即可。当然对于ShowFx和HideFx两个属性,提供几个选项并不够用,因为这些名字并不能让人马上明白它的实例效果,所以在几年前我做的另一个日期选择控件中,提供了这样的编辑器给用户设置弹出如图B-20所示的效果。

图B-20 可预览到效果的编辑器更加平易近人
DateChooserDesigner设计器的主要功能是提供设计时HTML代码和ActionList。由于在设计DateChooser控件类时,已经考虑了设计时HTML,所以GetDesignTimeHtml()方法简单组织DateChooser的几个呈现方法即可。
任务列表提供的全是属性设置的ActionItem,其中ShowPanel属性操作的是设计器的属性:
class DateChooserActionList : DesignerActionList
{
DateChooserDesigner _designer;
public DateChooserActionList(DateChooserDesigner designer)
: base(designer.Component)
{
_designer = designer;
}
public override DesignerActionItemCollection GetSortedActionItems()
{
DesignerActionItemCollection list = new DesignerActionItemCollection();
list.Add(new DesignerActionTextItem("查看选择面板", "ShowPanel"));
list.Add(new DesignerActionPropertyItem("ShowPanel","显示","ShowPanel"));
// … …
return list;
}
public bool ShowPanel
{
get
{
return _designer.ShowPanel;
}
set
{
_designer.ShowPanel = value;
_designer.UpdateDesignTimeHtml();
}
}
//… …
}
其他智能任务都是对DateChooser控件进行操作,比如:
[TypeConverter(typeof(DateFormatConverter))]
public string DateFormat
{
get
{
return (Component as DateChooser).DateFormat;
}
set
{
TypeDescriptor.GetProperties(Component)["DateFormat"]
.SetValue(Component, value);
_designer.UpdateDesignTimeHtml();
}
}
需要注意的是,通过对DesignerActionList中的属性应用TypeConverterAttribute,可以控制智能任务面板中的属性设置方式,如上例中的DateFormat属性就会出现一个下拉选择UI,而不是一个简单的TextBox。在set中,使用(Component as DateChooser).DateFormat = value;这样的语句直接操作控件类,会出现设置不能马上生效的问题,用TypeDescriptor 的方式获得对象属性,并用SetValue对其设值则可解决这样的问题。
System.ComponentModel.TypeDescriptor类在设计时非常有用,利用它可为实现了IComponent接口的类动态编织属性(包括Attribute和Property)。以下是它的一些静态方法:
GetProperties()——返回组件的属性集合。
GetAttributes()——返回组件的Attribute集合。
GetEvents()——得到组件的事件集合。
AddAttribute()——向组件添加类级别Attribute。
CreateDesigner()——创建组件关联的设计器实例。
GetConverter()——得到组件关联的类型转换型实例。
GetEditor()——得到组件关联的编辑器。
GetDefaultEvent()——得到组件的默认事件。
GetDefaultProperty()——得到组件的默认属性。
除了TypeDescriptor,反射也可访问对象的元数据信息,更多信息可参考System.Reflection命名空间。







