8.2 实现模板控件
了解了ASP.NET框架下实现模板控件如此简单后,我们来实现一个模板控件——BookInfo,它使用模板的方式显示BookData数据。以下是使用BookInfo的一个例子,它有一个ItemTemplate模板,它的DataItem属性为BookData数据类型:
<tc:BookInfo ID="tc0470109491" runat="Server"
DataItem-Author="Nicholas C. Zakas, Jeremy McPeak, Joe Fawcett"
DataItem-Description="My favorite book" DataItem-ISBN="0470109491"
DataItem-Publisher="Wrox" DataItem-Title="Professional AJAX">
<ItemTemplate>
<div class="bookContainer">
<asp:Image runat="server" ID="imgBook" CssClass="bookCover"
AlternateText='<%# Container.DataItem.Title %>'
ImageUrl='<%# "~/images/"+Container.DataItem.ISBN+".png"%>'
/>
<div class="bookContent">
<h3><%# Container.DataItem.Title %></h3>
Written by:<%# Container.DataItem.Author %><br />
ISBN: #<%# Container.DataItem.ISBN %>
<div class="bookPublisher">
<%# Container.DataItem.Publisher %></div>
</div>
</div>
</ItemTemplate>
</tc:BookInfo>
运行时效果则由模板来决定,如图8-1所示。

图8-1 BookInfo的模板运行效果
8.2.1 BookData
首先实现BookInfo控件要用到的BookData类,它表示一本书的信息,为了在页面回传时,BookData数据能得到持久保存,BookData类需要实现IStateManager接口。
[TypeConverter(typeof(ExpandableObjectConverter))]
[Serializable]
public class BookData:IStateManager
{
[NotifyParentProperty(true)]
[Category("Data")]
public string ISBN
{
get
{
if (ViewState["ISBN"] == null)
return "";
return (string)ViewState["ISBN"];
}
set
{
ViewState["ISBN"] = value;
}
}
[NotifyParentProperty(true)]
[Category("Data")]
public string Title
{
get
{
if (ViewState["Title"] == null)
return "";
return (string)ViewState["Title"];
}
set
{
ViewState["Title"] = value;
}
}
[NotifyParentProperty(true)]
[Category("Data")]
public string Author
{
get
{
if (ViewState["Author"] == null)
return "";
return (string)ViewState["Author"];
}
set
{
ViewState["Author"] = value;
}
}
[NotifyParentProperty(true)]
[Category("Data")]
public string Publisher
{
get
{
if (ViewState["Publisher"] == null)
return "";
return (string)ViewState["Publisher"];
}
set
{
ViewState["Publisher"] = value;
}
}
[NotifyParentProperty(true)]
[Category("Data")]
public string Description
{
get
{
if (ViewState["Description"] == null)
return "";
return (string)ViewState["Description"];
}
set
{
ViewState["Description"] = value;
}
}
public BookData()
{
}
public BookData(string isbn, string title, string author,
string publisher, string description)
{
ISBN = isbn;
Title = title;
Author = author;
Publisher = publisher;
Description = description;
}
#region IStateManager 成员
private bool _isTrackViewState;
private StateBag _viewState;
[Browsable(false)]
[DesignerSerializationVisibility(
DesignerSerializationVisibility.Hidden)]
public StateBag ViewState
{
get
{
if (_viewState == null)
{
_viewState = new StateBag(false);
if (_isTrackViewState)
{
((IStateManager)_viewState).TrackViewState();
}
}
return _viewState;
}
}
[Browsable(false)]
[DesignerSerializationVisibility(
DesignerSerializationVisibility.Hidden)]
public bool IsTrackingViewState
{
get
{
return _isTrackViewState;
}
}
public void LoadViewState(object state)
{
if (state != null)
{
((IStateManager)ViewState).LoadViewState(state);
}
}
public object SaveViewState()
{
if (this._viewState != null)
{
return ((IStateManager)_viewState).SaveViewState();
}
return null;
}
public void TrackViewState()
{
this._isTrackViewState = true;
if (_viewState != null)
{
((IStateManager)_viewState).TrackViewState();
}
}
#endregion
}
BookData拥有5个属性,这5个属性都保存在一个叫_viewState的StateBag变量中,_viewState中保存的数据要持久化到页面__VIEWSTATE隐藏域中,这需要参与到页面的视图状态管理过程中,这个过程将在BookInfo控件中实现。
BookData关联ExpandableObjectConverter类型转换器,这样BookData类型的属性在属性窗口中显示为可展开的属性,如图8-2所示,便于用户编辑。

图8-2 可展开属性
8.2.2 BookInfo控件
BookInfo控件用来显示图书信息,所以它拥有一个DataItem属性,关联一个BookData实例。然后它用ItemTemplate模板显示DataItem的数据。
由于模板的原因,BookInfo控件会拥有多个子控件,所以BookInfo从CompositeControl继承。
[Designer(typeof(BookInfoDesigner))]
public class BookInfo:CompositeControl
{
BookData _dataItem;
[DesignerSerializationVisibility(
DesignerSerializationVisibility.Content)]
[Category("Data")]
public virtual BookData DataItem
{
get
{
if (_dataItem == null)
{
_dataItem = new BookData();
((IStateManager)_dataItem).TrackViewState();
}
return _dataItem;
}
set
{
_dataItem = value;
}
}
private ITemplate _itemTemplate;
[Browsable(false)]
[TemplateContainer(typeof(BookInfo))]
[PersistenceMode(PersistenceMode.InnerProperty)]
public virtual ITemplate ItemTemplate
{
get { return _itemTemplate; }
set
{
_itemTemplate = value;
base.ChildControlsCreated = false;
}
}
protected override HtmlTextWriterTag TagKey
{
get
{
return HtmlTextWriterTag.Div;
}
}
protected override void CreateChildControls()
{
Controls.Clear();
if (_itemTemplate != null)
_itemTemplate.InstantiateIn(this);
else
base.CreateChildControls();
ChildControlsCreated = true;
}
#region ViewState
protected override object SaveViewState()
{
object[] states = new object[2];
states[0] = base.SaveViewState();
states[1] = ((IStateManager)DataItem).SaveViewState();
return states;
}
protected override void LoadViewState(object savedState)
{
object[] states = (object[])savedState;
base.LoadViewState(states[0]);
((IStateManager)DataItem).LoadViewState(states[1]);
}
protected override void TrackViewState()
{
base.TrackViewState();
((IStateManager)DataItem).TrackViewState();
}
#endregion
}
注意对两个属性的处理,DataItem属性在设计时被序列化为Content,所以它的子属性被序列化成DataItem-XXX:
<tc:BookInfo ID="tc0470109491" runat="Server"
DataItem-Author="Nicholas C. Zakas, Jeremy McPeak, Joe Fawcett"
DataItem-Description="My favorite book" DataItem-ISBN="0470109491"
DataItem-Publisher="Wrox" DataItem-Title="Professional AJAX">
… … </tc:BookInfo>
而ItemTemplate显示在属性窗口中却没有任何意义,所以干脆不要让它显示在属性窗口中。
由于BookData需要进行状态管理,所以我们重写了与视图状态管理有关的方法,让控件在处理自己视图状态的同时,BookData也有机会管理它的视图状态。
特别需要注意的是重写的CreateChildControls()。在CreateChildControls()方法中,使用_itemTemplate的InstantiateIn()方法生成控件的子控件。在实现化的过程中,模板内容的容器是实现了INamingContainer接口的BookInfo控件,所以模板实例出的子控件的ID将体现命名容器内部控件ID的规律:
<img id="tc0470109491_imgBook" class="bookCover" src="images/0470109491.png" alt="Professional AJAX" style="border-width:0px;" />
此外,传递给InstantiateIn()方法的参数类型应该与ItemTemplate上应用的TemplateContainerAttribute指定的类型相匹配。
8.2.3 BookInfoDesigner
并不是所有的页面开发人员都习惯在源代码视图设计控件,所以我们需要为控件提供更好的设计时支持,如图4-3所示。

图8-3 模板的设计时支持
public class BookInfoDesigner : CompositeControlDesigner
{
public override void Initialize(IComponent component)
{
base.Initialize(component);
SetViewFlags(ViewFlags.TemplateEditing, true);
}
public override string GetDesignTimeHtml()
{
BookInfo control = Component as BookInfo;
if (control != null)
{
if (control.ItemTemplate == null)
{
return CreatePlaceHolderDesignTimeHtml(
"Please edit ItemTemplate");
}
}
return base.GetDesignTimeHtml();
}
TemplateGroupCollection _tempgc;
public override TemplateGroupCollection TemplateGroups
{
get
{
if (_tempgc == null)
{
_tempgc = base.TemplateGroups;
//定义模板组和声明模板
TemplateGroup templates =
new TemplateGroup("ItemTemplate");
templates.AddTemplateDefinition(
new TemplateDefinition(this, "ItemTemplate",
Component, "ItemTemplate",false));
_tempgc.Add(templates);
}
return _tempgc;
}
}
}
除了重写TemplateGroups属性,还需要重写Initialize()方法调用SetViewFlags(ViewFlags. TemplateEditing, true)方法才能让设计时支持所见即所得的编辑模板。
此外,我们重写了GetDesignTimeHtml(),这样在用户没有提供模板内容时,控件显示默认的占位块界面,如图8-4所示。
![]()
图8-4 模板为空时的设计时效果
最后,编译控件,将BookInfo添加到页面中,为它设计ItemTemplate模板。需要注意的是,需要调用BookInfo的DataBind()方法才能使模板内容结合控件的数据。
protected void Page_Load(object sender, EventArgs e)
{
this.tc0470109491.DataBind();
}







