带有设计时支持的增强型 Web Tab 控件
一个完全可定制的 Web Tab 控件。
引言
两个月前,我写了我在 CodeProject 的第一篇文章,shSimplePanel,一个具有良好设计时支持的控件。现在,我完成了另一个名为 shTabControl 的控件。这个 Web 控件是用 ASP.NET 2.0 编写的,基于 Mohammed Mahmoud 的Web Tab Control和shSimplePanel,但这个控件增强了设计时支持,外观和感觉也更好。我希望它能对您的项目有所帮助。如果您发现我的代码中有任何错误,或者我的英语不好,请不要犹豫给我发邮件。
背景
如果您查看 Mohammed Mahmoud 的文章,您会发现我采用了相同的架构来构建这个控件,但只使用了 Table
、TableRow
和 TableCell
。看一下这个
shTabControl
是一个复合控件,以 Table
控件作为主容器。第一行是 RowHeader
,它包含一个 TableCell
来显示每个添加的标签页,最后,我放置了一个空的 TableCell
来显示空白区域。接下来,为每个标签页添加一个 TableRow
- TableCell
,因为它是子控件的容器。通过这种架构,我们可以轻松地添加一个用于设置 TitlePosition
为 Top
或 Bottom
的方便属性。
此外,我们还可以为每个添加的标签页在 CellPage 中添加一些样式,例如不透明背景、鼠标悬停、标签页图片已启用和标签页图片已禁用效果。稍后我将解释这些属性。
使用代码
实现代码非常容易,借助 VS.NET 2005 的 IntelliSense。shTabControl
包含另一个名为 shTabPage
的控件,它在 ContentTemplate
属性内的 Controls
属性中持久化子控件。
<shw:shTabControl ID="TabControl1" runat="server" CurrentPage="0"
Height="350px" HeightCell="26px" TitlePosition="Top" Width="515px">
<TabPages>
<shw:shTabPage ID="TabPage0" runat="server" BackColor="#C0C000"
Height="350px"Text="Page0" Width="81px">
<ContentTemplate>
The contents and childs control here...
</ContentTemplate>
</shw:shTabPage>
</TabPages>
</shw:shTabControl>
shTabControl
具有以下属性来控制其在设计时的外观和感觉
TabPages
:shTabPage
类的集合属性。HeightCell
:所有CellHeader
的高度。TitlePosition
:CellHeader
的位置。必须是Top
或Bottom
。ShowBorderCell
:一个标志,指示CellHeader
是否可以显示其边框。ImageHeader
:第一个(顶部)或最后一个(底部)RowPage
上的背景图像。CurrentPage
:当前选中的页面。
为了实现这个控件,我使用了一些 JavaScript 来生成客户端行为和回发 :-)。shTabControl
使用 UITypeEditor
实现 shTabPage
的集合属性。所有这些概念我都会在这里解释。
SetVisiblePage JavaScript 客户端行为
SetVisiblePage
是一个 JavaScript 函数,用于生成回发并在回发后显示选中的页面。
<script language="javascript">
function SetVisiblePage(shTabName, value)
{
var obj= document.getElementById(shTabName + "_" + "hdCurrentPage");
if(obj!=null)
{
obj.value = value;
obj.form.submit();
}
}
</script>
此函数用于每个 _CellHeader
的 OnClick
事件,其中 shTabName
是一个包含 shTabControl
的 ClientID
的字符串,而 value
参数是当前选中的页面。我将此字符串插入到项目的资源中。此函数在运行时在 protected Render
方法内渲染。
//On run time write the javascript
if (!DesignMode)
{
string script = ShWebTabControl.Properties.Resources.SetVisiblePageScript;
script = script.Replace("SetVisiblePage", ClientID + "SetVisiblePage");
writer.Write(script);
... ...
}
我使用 Replace
方法来添加 shTabControl
的 ClientID
属性,因为我想在页面加载时生成一个唯一的函数;但是,如果您愿意,也可以删除它。此函数与 _CellHeader
的实现集成在一个名为 CreateCellHeader
的 private
方法中。
private TableCell CreateCellHeader(shTabPage item, int nTabPage)
{
TableCell CellHeader = new TableCell();
CellHeader.ID = "_CellHeader" + nTabPage.ToString();
//Please see the code for more information
CellHeader.Attributes["onclick"] = ClientID + "SetVisiblePage(\"" + ClientID.ToString() +
"\"," + nTabPage.ToString() + ");";
//Please see the code for more information
return CellHeader;
}
CreateCellHeader
从 shTabPage
创建 TableCell
的结构。所有 CellHeader
都有一个唯一的 ID。shTabPage
类实现了配置每个 CellHeader
的所有属性。
shTabPage 类
shTabPage
类包含配置 shTabControl
的每个标签页的所有属性。此类继承自 WebControl
、INamingContainer
和 IStateManager
,用于实现属性的 ViewState,并在设计和运行时持久化更改。
[ToolboxData("<{0}:shTabPage runat="server">")]
[NonVisualControlAttribute()]
[ParseChildren(true)]
[PersistChildren(false)]
public class shTabPage : WebControl, IStateManager, INamingContainer
{
private bool _isTrackingViewState;
private ITemplate _contentTemplate;
public shTabPage():base(HtmlTextWriterTag.Div) { }
//Properties Sections
...
//IStateManager Members
//Please see the code for more methods of IStateManager
void IStateManager.TrackViewState()
{
_isTrackingViewState = true;
if (ViewState != null)
((IStateManager)ViewState).TrackViewState();
}
internal void SetDirty()
{
if (ViewState != null)
{
ICollection Keys = ViewState.Keys;
foreach (string key in Keys)
{
ViewState.SetItemDirty(key, true);
}
}
}
}
此类基于 DIV
。此类用于获取其属性,然后将其应用于 shTabControl
。之所以使用 IStateManager
接口,是因为在实现 shTabPage
集合时,我们需要强制 ViewState。SetDirty
方法执行此操作。这里展示了 shTabPage
的一些属性
Text
:CellHeader
的标题文本。TitleWidth
:CellHeader
的宽度。Opacity
:CellPage
的不透明度。ImageEnable
:CellHeader
选中时的图像。ImageDisable
:CellHeader
未选中时的图像。
shTabPageCollection 类
此类继承自 CollectionBase
并实现 IStateManager
接口。使用 IStateManager
是因为我们希望保存每个 shTabPage
的状态。LoadViewState
和 SaveViewState
执行主要工作。有关更多信息,请参见代码。
public int Add(shTabPage shtabpage)
{
List.Add(shtabpage);
if (_isTrackingViewState)
{
((IStateManager)shtabpage).TrackViewState();
shtabpage.SetDirty();
}
return List.Count - 1;
}
object IStateManager.SaveViewState()
{
if (_saveAll == true)
{
object[] states = new object[Count];
for (int i = 0; i < Count; i++)
{
shTabPage shtabpage = (shTabPage)List[i];
shtabpage.SetDirty();
states[i] = ((IStateManager)shtabpage).SaveViewState();
}
if (Count > 0)
return states;
else
return null;
}
else
{
ArrayList indices = new ArrayList();
ArrayList states = new ArrayList();
for (int i = 0; i < Count; i++)
{
shTabPage shtabpage = (shTabPage)List[i];
object state = ((IStateManager)shtabpage).SaveViewState();
if (state != null)
{
states.Add(state);
indices.Add(i);
}
}
if (indices.Count > 0)
return new Pair(indices, states);
return null;
}
}
void IStateManager.TrackViewState()
{
_isTrackingViewState = true;
for(int i=0; i < Count; i++)
{
shTabPage shtabpage = (shTabPage)List[i];
((IStateManager)shtabpage).TrackViewState();
}
}
此类的一些方法检查 _isTrackViewState
是否为 true
。当此标志打开时,将调用 TrackViewState
方法,然后调用 SetDirty
方法(参见 Add
、Clear
、Insert
方法),这会导致状态在 shTabPageCollection
的 view state 中持久化。
当 _saveAll
为 true
时,SaveViewSate
将所有项保存在一个 object
s 数组中。当 _saveAll
为 false
时,SaveViewState
只保存已更改的项,并将其存储在两个 ArrayList
中:第一个存储已更改项的索引,第二个存储状态,然后返回一个 Pair
对象,该对象包含 shTabPageCollection
的 view state。
LoadViewState
方法执行与此方法相反的逻辑。有关更多信息,请参见代码。
shTabControl 类
这是包含 shTabPageCollection
属性的主类。
public shTabPageCollection TabPages
{
get{
if (_TabPages == null){
_TabPages = new shTabPageCollection();
if (IsTrackingViewState)
((IStateManager)_TabPages).TrackViewState();
}
return _TabPages;
}
}
此属性保存在 shTabControl
的 ViewState 中。实现与我在shSimplePanel
中使用的相同;有关自定义状态管理的更多信息,请参见该文章。
shTabControl
继承自 CompositeControl
和 IPostBackDataHandler
,因为我们希望在 CurrentPage
属性更改时实现事件。此类使用一个隐藏字段来保存此属性。此控件是一个 HtmlInputHidden
,名为 hdCurrentPage
,它在 **SetVisiblePage 脚本** 和 IPostBackDataHandler
中使用。
protected virtual void OnCurrentPageChanged(EventArgs e)
{
EventHandler currentPageChangedHandler = (EventHandler)Events[EventCurrentPageChanged];
if (currentPageChangedHandler != null)
{
currentPageChangedHandler(this, e);
}
}
bool IPostBackDataHandler.LoadPostData(string postDataKey,
System.Collections.Specialized.NameValueCollection postCollection){
int current = CurrentPage;
string posted = postCollection[postDataKey + "$hdCurrentPage"];
if((posted != null) && (posted.Length>0))
CurrentPage = Convert.ToInt32(posted);
//CurrentPage state has changed
if (current != CurrentPage)
return true;
return false;
}
void IPostBackDataHandler.RaisePostDataChangedEvent()
{
//raise our event notifying the CurrentPage change
OnCurrentPageChanged(EventArgs.Empty);
}
当用户单击标签页时,脚本执行提交,LoadPostData
方法检查 hdCurrentPage
的值是否与 CurrentPage
属性不同;如果不同,则启动 RaisePostDataChangedEvent
。隐藏字段在执行回发时更改其名称。我在调试模式下运行页面时看到了这个消息。
此处展示了 shTabControl
的 CreateChildControls
方法。
protected override void CreateChildControls()
{
//See the code please
//Creates all Cell Header from tab pages
//and the Row of Content, then add to Main Table
int shTabPageIndex = 0;
TableRow RowHeader = new TableRow();
RowHeader.ID = "_RowHeader";
foreach (shTabPage item in TabPages)
{
TableCell CellHeader = CreateCellHeader(item, shTabPageIndex);
RowHeader.Cells.AddAt(shTabPageIndex, CellHeader);
TableRow RowContent = CreateRowContent(item, shTabPageIndex);
cTableMain.Rows.Add(RowContent);
++shTabPageIndex;
}
//Add a empty cell to Row Header
TableCell CellHeaderEmpty = CreateCellEmpty();
RowHeader.Cells.AddAt(shTabPageIndex, CellHeaderEmpty);
//Add on Top or Bottom the Row Header created
if (TitlePosition == shTitlesPosition.Top)
cTableMain.Rows.AddAt(0, RowHeader);
else
cTableMain.Rows.Add(RowHeader);
//Add the controls to Controls Property
Controls.Add(hdCurrentPage);
Controls.AddAt(0,cTableMain);
}
此方法创建 shTabControl
的结构。foreach
循环执行 CreateCellHeader
、CreateRowContent
,最后执行 CreateCellEmpty
。这些私有方法用于创建 RowHeader
的单元格标题以及创建 RowPage
-CellPage
的内容。最后,CreateHeaderEmpty
在所有单元格标题的末尾创建一个没有边框的单元格。
一个名为 SetTabPage
的方法在 Render
方法中调用,用于在设计/运行时用户单击时设置 RowPage
-CellPage
的可见性。在此方法中,我实现了鼠标悬停效果。
最后,我将展示 shTabControlControlDesigner
。此类继承自 CompositeControlDesigner
,并基于 Mohammed Mahmoud 的WebTabControl
和 shSimplePanel
。首先,我实现了一个 TemplateGroups
public override TemplateGroupCollection TemplateGroups
{
get{
TemplateGroupCollection collection = new TemplateGroupCollection();
TemplateGroup group = new TemplateGroup(_shTabControl.ID);
for (int i = 0; i < _shTabControl.TabPages.Count; i++){
TemplateDefinition definition = new TemplateDefinition(
this,HEADER_PREFIX + i.ToString(),
_shTabControl.TabPages[i], "ContentTemplate", false);
group.AddTemplateDefinition(definition);
}
collection.Add(group);
return collection;
}
}
现在,在设计时,我们在 CreateChildControls
方法中设置用作模板的区域。在此方法中,我们检查 TitlePosition
属性是 Top
还是 Bottom
,以设置 CellHeader
的源。如果它是 Top
,则第一行是 RowHeader
;否则,最后一行为 RowHeader
。当用户单击时,将触发 OnClick
,然后我们检查单击是否发生在 HeaderCell
上。在这里,我们在 OnClick
事件中调用 UpdateDesignTimeHtml
时调用 GetDesignTimeHtml
方法。GetDesignTimeHtml
方法基于 CurrentPage
设置一个可编辑区域。
protected override void CreateChildControls()
{
//See the code ...
for (int i = 0; i < _shTabControl.TabPages.Count; i++){
if(_shTabControl.TitlePosition == shTabControl.shTitlesPosition.Top)
Tbl.Rows[0].Cells[i].Attributes[
DesignerRegion.DesignerRegionAttributeName] = i.ToString();
else
Tbl.Rows[Tbl.Rows.Count-1].Cells[i].Attributes[
DesignerRegion.DesignerRegionAttributeName]=i.ToString();
}
//set the editable region
if (_shTabControl.CurrentPage != -1){
if(_shTabControl.TitlePosition == shTabControl.shTitlesPosition.Top)
Tbl.Rows[1 + _shTabControl.CurrentPage].Cells[0].Attributes[
DesignerRegion.DesignerRegionAttributeName]=
_shTabControl.TabPages.Count.ToString();
else
Tbl.Rows[_shTabControl.CurrentPage].Cells[0].Attributes[
DesignerRegion.DesignerRegionAttributeName]=
_shTabControl.TabPages.Count.ToString();
}
}
public override string GetDesignTimeHtml(DesignerRegionCollection regions)
{
this.CreateChildControls();
for (int i = 0; i < _shTabControl.TabPages.Count; i++)
regions.Add(new DesignerRegion(this, HEADER_PREFIX + i.ToString()));
if (_shTabControl.CurrentPage != -1){
regions.Add(new EditableDesignerRegion(this,
CONTENT_PREFIX + _shTabControl.CurrentPage.ToString(),false));
regions[_shTabControl.CurrentPage].Highlight = true;
}
return base.GetDesignTimeHtml(regions);
}
结论
此控件是使用 viewstate 和 ControlDesigner
实现设计时支持的一个示例。shTabControl
的主要创意来源于WebTabControl
。希望这个控件对您的项目有所帮助。编码愉快 :-)
更新
- 2006-02-22:我重写了
shTabPage
类以支持StateBag
变量。请尝试将EnableViewState
属性设置为false
以支持TabPage
内的控件。请检查一下 :-)