创建基于 Web 的选项卡控件






4.33/5 (10投票s)
2006年5月12日
6分钟阅读

155251

1640
创建基于 Web 的选项卡控件,具有多个选项卡。
引言
我一直在想,为什么不提供一个简单的 Web 选项卡控件,开发人员可以根据自己的需求进行扩展。当然,有很多第三方控件提供了丰富而高级的选项卡控件,但我找不到太多关于这个主题的文章。
当表单中有大量控件时,拆分 Web 表单控件确实是必要的,它也有助于根据特定类别组织表单。
在本文中,我将更侧重于控件设计器,以便开发人员可以提供与 Windows Forms Tab
控件类似的外观和感觉。虽然在运行时提供外观和感觉是一个显而易见的要求,但这只是一个通过调整 HTML 表格单元格颜色和一些图像来提供 3D 外观的问题。
背景
简单来说,选项卡控件包含两部分:选项卡标题和选项卡正文。在我的解决方案中,选项卡控件表示为一个表格,其中单元格包含标题和选项卡正文,类似于以下表格:
| ||||
选项卡 1 的正文 | ||||
选项卡 2 的正文 | ||||
.. | ||||
选项卡 n 的正文 |
而在运行时,选项卡控件正是这样表示的,不同之处在于只有活动选项卡的正文可见,而其他所有选项卡都将被隐藏。
我们的选项卡控件的设计和运行时视图
基本上,选项卡控件将由一个选项卡页集合 MyTabPageCollection
组成,它是一个 MyTabPage
控件的集合。此控件包含两个主要属性,即选项卡标题 Title
和选项卡正文 TabBody
,类型为 ITemplate
。System.Web.UI.ITemplate
将允许设计和保存其他控件。
public class MyTabPage : System.Web.UI.WebControls.PlaceHolder
{
private string _title = "";
private ITemplate _tabBody;
public string Title
{
get { return _title; }
set { _title = value; }
}
[
PersistenceMode(PersistenceMode.InnerProperty),
DefaultValue(null),
Browsable(false)
]
public virtual ITemplate TabBody
{
get { return _tabBody; }
set { _tabBody = value; }
}
}
选项卡控件 MyTabControl
是 CompositeControl
的子类,它包含所有选项卡,并包含一些属性来在设计时通过 CurrentDesignTab
属性,在运行时通过 SelectedTab
属性来保存活动选项卡。这些属性用于保存活动选项卡的索引。选项卡控件设计器 MyTabControlDesigner
将根据此索引在选项卡模板之间切换并激活它们。多选项卡保存在 MyTabPageCollection
类型的 TabPages
集合中;此集合允许定义多个选项卡。MyTabControl
实现两个主要方法:OnPreRender
和 CreateChildControls
。
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (DesignMode)
{
_tabPages[_currentDesignTab].TabBody.InstantiateIn(this);
}
}
protected override void CreateChildControls()
{
// Always start with a clean form
Controls.Clear();
// Create a table using the control's declarative properties
Table tabControlTable = new Table();
tabControlTable.CellSpacing = 1;
tabControlTable.CellPadding = 0;
tabControlTable.BorderStyle = BorderStyle;
tabControlTable.Width = this.Width;
tabControlTable.Height = this.Height;
tabControlTable.BackColor = ColorTranslator.FromHtml("inactiveborder");
//keep the selected tab index in a an attribute
tabControlTable.Attributes.Add("ActiveTabIdx", _selectedTab.ToString());
BuildTitles(tabControlTable);
BuildContentRows(tabControlTable);
// Add the finished tabControlTable to the Controls collection
Controls.Add(tabControlTable);
}
在设计时,OnPreRender
将实例化由模板表示的活动选项卡,其索引为 _currentDesignTab
在选项卡控件中,而 CreateChildControls
方法负责根据前面描述的布局绘制控件内容。此外,它还会将 ActiveTabIdx
公开给表示选项卡容器的表的属性。选项卡标题将通过 BuildTitles
方法呈现,该方法会遍历 TabPages
集合并将标题绘制为表格单元格。此外,它还会创建 OnClick
事件处理程序调用 JavaScript 函数 ShowTab
,该函数负责显示活动选项卡。
我们的选项卡控件由保存标题和选项卡内容的表格表示。在呈现标题后,我们需要呈现选项卡内容。BuildContentRows
方法负责呈现内容,其中每个选项卡正文由表中的一个单元格表示。请注意,由于表的第一行将保存标题,因此选项卡内容行的索引将从 1 开始,因此活动选项卡位于行索引 ActiveTabIdx+1
处。
对于设计时视图,我们不需要实例化所有选项卡,只需要实例化活动选项卡,正如您在 BuildContentRows
方法中所看到的。
private void BuildContentRows(Table tabControlTable)
{
// Create content row(s)
if (DesignMode)
{
TableRow contentRow = new TableRow();
TableCell contentCell = BuildContentCell(contentRow);
_tabPages[_currentDesignTab].TabBody.InstantiateIn(contentCell);
tabControlTable.Rows.Add(contentRow);
}
else
{
int counter = 0;
foreach (MyTabPage tabPage in _tabPages)
{
TableRow contentRow = new TableRow();
TableCell contentCell = BuildContentCell(contentRow);
if (tabPage.TabBody != null)
{
tabPage.TabBody.InstantiateIn(contentCell);
}
//only the selected tab body should be visible
if (_selectedTab == counter)
{
contentRow.Style["display"] = "block";
}
else
{
contentRow.Style["display"] = "none";
}
contentRow.Cells.Add(contentCell);
tabControlTable.Rows.Add(contentRow);
counter++;
}
}
}
对于运行时视图,该过程将遍历所有选项卡并在内容行中实例化选项卡正文。
在客户端切换选项卡
当用户单击选项卡标题时,客户端事件处理程序 ShowTab
将切换选项卡,知晓被单击的选项卡及其当前索引;内联注释提供了此过程工作方式的详细描述。
<script type="text/javascript" language="javascript">
function ShowTab(tabTitleCell, idx)
{
//get the table which holds the tabs
var tabsTable = tabTitleCell.parentElement.parentElement.parentElement;
//what is the active tab index
var activeTabIdx = Number(tabsTable.getAttribute("ActiveTabIdx"));
//give the inactive appearance to the previous active tab
tabsTable.rows[0].cells[activeTabIdx].style["backgroundColor"] = "inactiveborder";
tabsTable.rows[0].cells[idx].style["backgroundColor"] = "darkgray";
//since the tabs body contained in rows with
//index as the same of the tab title link plus 1,
//then we can hide the row that holds the active tab.
tabsTable.rows[activeTabIdx + 1].style.display = "none";
//show the active tab body
tabsTable.rows[idx + 1].style.display = "";
//keep the new active tab in the attribute ActiveTabIdx
tabsTable.setAttribute("ActiveTabIdx", idx);
}
<script>
管理选项卡控件模板并切换设计视图
选项卡控件设计器应负责
- 声明设计区域。
- 切换选项卡。
- 持久化选项卡正文,并在设计器中查看它们。
CompositeControlDesigner
提供了基本设计器,允许控制可编辑区域并创建子控件。MyTabControlDesigner
继承自 CompositeControlDesigner
,以支持选项卡控件的设计视图。该设计器包含一个私有引用 tabControl
,指向选项卡控件。此引用在 Initialize
方法中初始化。此引用可以通过设计器逻辑来引用选项卡控件。
声明设计区域
在获取设计时 HTML 时,重写的 GetDesignTimeHtml
方法将为所有标题单元格添加设计区域;区域名称以前缀 HEADER_PREFIX
开头,并以选项卡页索引扩展。这样,我们就可以从区域名称中提取选项卡索引。然后,创建一个具有与标题前缀 CONTENT_PREFIX
和当前活动选项卡索引相似的命名格式的设计区域。
public override String GetDesignTimeHtml(DesignerRegionCollection regions)
{
int i = 0;
foreach (MyTabPage tabPage in tabControl.TabPages)
{
regions.Add(new DesignerRegion(this,
HEADER_PREFIX + i.ToString()));
i++;
}
EditableDesignerRegion editableRegion =
new EditableDesignerRegion(this,
CONTENT_PREFIX +
tabControl.CurrentDesignTab, false);
regions.Add(editableRegion);
regions[tabControl.CurrentDesignTab].Highlight = true;
return base.GetDesignTimeHtml();
}
所有选项卡标题单元格都将用保存选项卡索引的设计器区域属性进行标记。此外,内容单元格也将被标记为设计区域。
protected override void CreateChildControls()
{
base.CreateChildControls();
Table table = (Table) tabControl.Controls[0];
if (table != null)
{
for (int i = 0; i < tabControl.TabPages.Count; i++)
{
table.Rows[0].Cells[i].Attributes[
DesignerRegion.DesignerRegionAttributeName] =
i.ToString();
}
table.Rows[1].Cells[0].Attributes[
DesignerRegion.DesignerRegionAttributeName] =
(tabControl.TabPages.Count).ToString();
}
}
切换选项卡
当用户单击控件时,OnClick
将捕获事件。在这里,只有当单击的区域是已知区域时,代码才会从单击的标题的区域名称中提取选项卡索引,并将其设置为选项卡控件属性 CurrentDesignTab
。然后,UpdateDesignTimeHtml
将相应地更新设计视图。
protected override void OnClick(DesignerRegionMouseEventArgs e)
{
if (e.Region == null)
return;
if (e.Region.Name.IndexOf(HEADER_PREFIX) != 0)
return;
if (e.Region.Name.Substring(HEADER_PREFIX.Length) !=
tabControl.CurrentDesignTab.ToString())
{
tabControl.CurrentDesignTab =
int.Parse(e.Region.Name.Substring(HEADER_PREFIX.Length));
base.UpdateDesignTimeHtml();
}
}
持久化选项卡正文,并在设计器中查看
在切换选项卡时,设计器需要获取表示活动选项卡的活动模板。从活动区域名称,我们可以获得选项卡索引,通过从 tabControl.TabPages[tabIndex].TabBody
获取活动选项卡,并通过 ControlPersister.PersistTemplate
方法返回 HTML。请参见 GetEditableDesignerRegionContent
方法。
public override string GetEditableDesignerRegionContent(EditableDesignerRegion region)
{
IDesignerHost host = (IDesignerHost)
Component.Site.GetService(typeof(IDesignerHost));
if (host != null && tabControl.TabPages.Count > 0)
{
ITemplate template = tabControl.TabPages[0].TabBody;
if (region.Name.StartsWith(CONTENT_PREFIX))
{
int tabIndex = int.Parse(region.Name.Substring(
CONTENT_PREFIX.Length));
template = tabControl.TabPages[tabIndex].TabBody;
}
if (template != null)
return ControlPersister.PersistTemplate(template, host);
}
return String.Empty;
}
对模板的任何更改都应反映到已编辑选项卡的 TabBody
。ControlParser.ParseTemplate
方法将从设计内容中实例化一个模板,并通过知道区域名称,我们可以获取选项卡索引,然后用内容模板更新 TabBody
。请参见 SetEditableDesignerRegionContent
方法。
public override void SetEditableDesignerRegionContent(
EditableDesignerRegion region, string content)
{
if (content == null)
return;
IDesignerHost host = (IDesignerHost)
Component.Site.GetService(typeof(IDesignerHost));
if (host != null)
{
ITemplate template = ControlParser.ParseTemplate(host, content);
if (template != null)
{
if (region.Name.StartsWith(CONTENT_PREFIX))
{
int tabIndex = int.Parse(
region.Name.Substring(CONTENT_PREFIX.Length));
tabControl.TabPages[tabIndex].TabBody = template;
}
}
}
}
最后
我并没有在这个控件的编码上做太多工作,但我认为我提供了创建基于 Web 的选项卡控件的基础。希望这篇文章对您有所帮助。此外,我将留给您的创意来改进呈现控件和切换选项卡的逻辑。开发一个脚本对象来管理客户端的选项卡控件,提供属性和选项卡集合,并提供一些方法来自动化切换、显示、禁用和启用选项卡的过程,这将是很好的。
如果您有其他想法或改进此代码的方法,请随时使用此代码并按您喜欢的方式进行更新。如果您能告诉我您的改进之处,我将不胜感激。