ASP.NET 嵌套 Repeater 控件






3.79/5 (9投票s)
2005年12月8日
6分钟阅读

95658

1688
一个服务器控件,其原理与 ASP.NET Repeater 类似,但可以处理递归(或分层)数据。
引言
每次我不得不使用 ASP.NET Repeater
控件处理递归数据时,我都会感到沮丧。没有简单的方法可以利用 Repeater
控件为这些数据提供数据绑定功能,例如,当我需要显示一个可能拥有多个“子”节点且具有任意数量子级别数据的树视图时。
当然,在简单的场景中,当只有两到三个数据级别时,我可以在我的 WebForm 上放置多个 Repeater
,在数据源中创建适当的 DataRelation
s,这样工作就完成了。但是,如果我无法提前知道需要显示多少子级别的数据,这种解决方案就无法使用。即使可以,这种解决方案也不够优雅:所有这些 Repeater
的 <ItemTemplate>
部分都是相同的。应该有一个简单的方法来声明一个控件,在一个 <ItemTemplate>
部分中,该部分对所有数据都相同,并让该控件根据当前节点的子级别以不同的方式进行处理。这就是为什么我决定开发一个新控件,我称之为 NestedRepeater
。
我希望这个控件支持声明式语法(即,在我的 WebForm 上声明一个 <ItemTemplate>
部分,并将所有控件都放在里面),而不是被迫使用代码隐藏。这个控件不需要提前知道它必须处理多少子级别的数据。
背景
请注意,本文的“工作原理”部分假设您熟悉模板化数据绑定控件(有关更多详细信息,请参阅 MSDN)。
如何使用 NestedRepeater
作为本文的示例,我将使用 NestedRepeater
来显示物种的动物分类。我想在视觉上渲染这个分类中的层次结构。
数据源
对于我打算显示的动物分类,数据来自以下名为 Animals
的 SQL 表:

可以有任意数量的列,但这里显示的这三列是 NestedRepeater
所必需的(请注意,您可以为这些列指定任何名称,因为您将像对待任何其他控件一样“DataBind”数据)。
ANI_ID
:主键,NOT NULL
ANI_NAME
:将显示的数据ANI_PARENT
:外键,引用ANI_ID
NestedRepeater
公开一个名为 DataSource
的属性。
public virtual DataSet DataSource;
正如您所见,DataSource
的类型是 DataSet
而不是 object
。
您只需像平常一样进行数据绑定。
SqlCommand cmd = new SqlCommand();
SqlConnection cnx = new SqlConnection(myCnxString);
cmd.Connection = cnx;
cmd.CommandText = "select * from ANIMALS";
SqlDataAdapter da = new SqlDataAdapter(cmd);
DataSet ds = new DataSet();
da.Fill(ds,"ani");
然后,您创建一个 DataRelation
来反映主键/外键关系。
ds.Relations.Add(RelationName,
ds.Tables[0].Columns["ANI_ID"],
ds.Tables[0].Columns["ANI_PARENT"]);
此时,NestedRepeater
需要两个属性:
protected NestedRepeater myRep;
myRep.RelationName = RelationName;
myRep.RowFilterTop = "ANI_PARENT is null";
myRep.RelationName
是您在DataSet
中为DataRelation
指定的名称。myRep.RowFilterTop
告诉NestedRepeater
如何确定哪些记录将成为顶级节点。在这里,最顶级的节点是ANI_PARENT
列为null
的记录。在我的 SQL 表中,有两个顶级节点:Invertebrates
和Vertebrates
。
可选地,您可以使用 DataMamber
属性来指定表名。如果您不这样做,NestedRepeater
会假定数据在 ds.Tables[0]
中可用。
那么
myRep.DataSource = ds;
myRep.DataBind();
这里没有什么需要解释的。
在 WebForm 上
要在 WebForm 上使用 NestedRepeater
,您必须先注册程序集。
<%@ Register tagprefix="meg" Namespace="WebCustomControls"
Assembly="WebCustomControls"%>
如果您使用 Visual Studio,您必须在项目中添加对 *WebCustomControls.dll* 的引用,此行才能正常工作。
那么
<meg:NestedRepeater id=myRep runat=server>
<HeaderTemplate>
This is the animal classification. <br>
</HeaderTemplate>
<FooterTemplate>
The end.
</FooterTemplate>
<ItemTemplate>
<img src="https://codeproject.org.cn/pix.gif" height="10"
width="<%# (Container.Depth * 10) %>">
<%# (Container.DataItem as DataRow)["ANI_NAME"]%>
<br>
</ItemTemplate>
</meg:NestedRepeater>
在这里,Container
的类型是 NestedRepeaterItem
(此类将在“工作原理”部分讨论),它提供了当前上下文的详细信息,这些信息对于真正渲染分层视图至关重要。
例如,Container.Depth
告诉我们在层次结构中的深度。在最顶层,Container.Depth
为 0。在其正下方的一级是 1,然后是 2,依此类推。在这里,为了营造层次感,我使用深度来放置一个(不可见的)图像,其宽度与深度成正比。如果您想根据当前子级别个性化显示,可以检查此属性。
Container.DataItem
的类型始终是 DataRow
,因此我们可以直接进行类型转换,而不是使用速度较慢的 DataBinder.Eval(…)
版本。(请注意,我们必须导入 System.Data
命名空间。)
<HeaderTemplate>
和 <FooterTemplate>
部分的工作方式与 <asp:Repeater>
中的相同。在此示例中我没有使用这些功能,但它们都支持数据绑定。
然后,结果应如下所示:

工作原理
工作分为两个函数完成:
CreateControlHierachy
protected virtual void CreateControlHierarchy(bool createFromDataSource)
{
int nbTopNodes = 0;
DataView dv = null;
// HeaderTemplate
if (m_headerTemplate != null)
// Do we have a <HeaderTemplate> section ?
{
NestedRepeaterHeaderFooter header =
new NestedRepeaterHeaderFooter();
m_headerTemplate.InstantiateIn(header);
if (createFromDataSource)
header.DataBind();
Controls.Add(header);
}
// ItemTemplate
if (createFromDataSource &&
DataSource != null &&
DataSource.Tables.Count != 0)
{
DataTable tbSource;
if (DataMember != String.Empty)
tbSource = DataSource.Tables[DataMember];
else
tbSource = DataSource.Tables[0];
if (tbSource == null)
throw new ApplicationException("No valid" +
" DataTable in the specified position.");
/* When creating from the ViewState (on PostBack),
* we'll need to know how many nodes
* there are under each node. So, when creating
* from the datasource, we store this
* information in m_lstNbChildren,
* which we'll also save in the viewstate.
* */
m_lstNbChildren = new ArrayList(tbSource.Rows.Count);
dv = new DataView(tbSource);
if (m_rowFilterTop != String.Empty)
dv.RowFilter = m_rowFilterTop;
nbTopNodes = dv.Count;
m_lstNbChildren.Add(nbTopNodes);
}
else
{
m_lstNbChildren = (ArrayList)ViewState["ListNbChildren"];
m_current = 0;
nbTopNodes = (int)m_lstNbChildren[m_current++];
}
NestedElementPosition currentPos;
for(int i=0; i< nbTopNodes; ++i)
{
if (i==0 && i==nbTopNodes-1)
currentPos = NestedElementPosition.OnlyOne;
else if (i ==0)
currentPos = NestedElementPosition.First;
else if (i == nbTopNodes - 1)
currentPos = NestedElementPosition.Last;
else
currentPos = NestedElementPosition.NULL;
if(createFromDataSource)
CreateItem(dv[i].Row, 0, currentPos);
else
CreateItem(null, 0, currentPos++);
}
if (createFromDataSource)
ViewState["ListNbChildren"] = m_lstNbChildren;
// FooterTemplate
if (m_footerTemplate != null)
{
NestedRepeaterHeaderFooter footer =
new NestedRepeaterHeaderFooter();
m_footerTemplate.InstantiateIn(footer);
if (createFromDataSource)
footer.DataBind();
Controls.Add(footer);
}
ChildControlsCreated = true;
}
此函数在数据绑定时(或在 PostBack 时,在 ViewState 加载期间)被调用。它创建头部并确定顶级节点的数量。对于每个顶级节点,它调用 CreateItem
。最后,它创建尾部。
CreateItem
private void CreateItem(DataRow row, int depth, NestedElementPosition pos)
{
DataRow[] childRows;
int nbChildren=0;
if (m_itemTemplate != null)
{
NestedRepeaterItem item = new NestedRepeaterItem();
if (row != null)
{
childRows = row.GetChildRows(RelationName);
nbChildren = childRows.Length;
m_lstNbChildren.Add(nbChildren);
item.Position = pos;
item.NbChildren = childRows.Length;
item.Depth = depth;
}
else // we use the viewstate
{
nbChildren = (int)
m_lstNbChildren[m_current++];
childRows = new DataRow[nbChildren];
}
m_itemTemplate.InstantiateIn(item);
Controls.Add(item);
NestedRepeaterItemEventArgs args =
new NestedRepeaterItemEventArgs();
args.Item = item;
OnItemCreated(args);
if (row != null)
{
item.DataItem = row;
item.DataBind();
OnItemDataBound(args);
}
// Recursive call
NestedElementPosition currentPos;
for(int i =0; i< nbChildren; ++i)
{
if (i==0 && i==nbChildren-1)
currentPos = NestedElementPosition.OnlyOne;
else if (i ==0)
currentPos = NestedElementPosition.First;
else if (i == nbChildren-1)
currentPos = NestedElementPosition.Last;
else
currentPos = NestedElementPosition.NULL;
if (row != null)
CreateItem(childRows[i], depth + 1, currentPos);
else
CreateItem(null, depth + 1, currentPos);
}
}
}
此函数为数据源中找到的每一行数据实例化一个项模板。
m_template.InstantiateIn(item);
m_template
的类型是 ITemplate
。它是属性的后端变量。
public virtual ITemplate ItemTemplate
{
get{return m_itemTemplate;};
set{m_itemTemplate = value;};
}
当 WebForm 被 ASP.NET 解析时,<ItemTemplate>
部分被“转换”成一个实现了 ITemplate
接口的类(这个类对我们是隐藏的),并且这个类的实例被赋给 NestedRepeater
的 ItemTemplate
属性。这个类拥有 WebForm 开发者在此部分声明的所有控件。在我的示例中,有一个 <img>
标签和一个文字字符串。在方法 InstanteIn
(由 .NET 生成)中,.NET 会实例化这两个控件并将它们添加到项的 Controls
属性中。伪代码应如下所示:
// this code is generated by .NET. We don't see it
void InstantiateIn(Control container)
{
HtmlImage img = new HtmlImage();
// further initialisation here
. . .
LiteralControl lit = new LiteralControl();
// further initialisation here
. . .
container.Controls.Add(img);
container.Controls.Add(lit);
. . .
}
当调用 item.DataBind()
时,两个表达式
<%# (Container.Depth * 10) %>
和
<%# (Container.DataItem as DataRow)["ANI_NAME"]%>
都会被评估。我们可以引发 ItemDataBound
事件。
OnItemDataBound(args);
item
的类型是 NestedRepeaterItem
。此类收集此节点所需的必要信息。
item.Position = pos;
item.NbChildren = childRows.Length;
item.Depth = depth;
item.DataItem = row;
使用 Position
属性,页面开发者可以确定当前节点是其父的第一个子节点、最后一个子节点,还是唯一的子节点。Position
被定义为一个 enum
。
public enum NestedElementPosition
{
First, // current record is the first child of the immediate parent
Last, // current record is the last child of the immediate parent
OnlyOne, // current record is the only child of the immediate parent
NULL // None of the above
}
NbChildren
属性指示当前节点的直接子节点数量。
Depth
属性已在前面提到过,DataItem
属性与 .NET 中的其他控件相同。DataItem
属性始终是一个 DataRow
。
一旦项被添加到 NestedRepeater
的 Controls
属性中,就会为每个子节点调用 CreateItem
,并将级别增加一个。
由于我们使用的是递归函数,因此可以有任意多的数据级别:无需提前知道有多少子级别。也就是说:我们可以向 SQL 表添加另一个子级别,而无需更改或添加任何一行代码。
更新
在 NestedRepeater
和 NestedRepeaterItem
类中添加了一个名为 Items
的新属性。这允许以编程方式遍历项,如下所示:
// a NestedRepeater called myRepeater has been declared elsewhere...
foreach(NestedRepeaterItem item in myRepeater.Items)
{
DoSomething(item);
}
// DoSomething is a recursive function
private void DoSomething(NestedRepeaterItem current)
{
// do whatever is required with the current item
// * * *
// then call DoSomething recursively for all sub-items
foreach(NestedRepeaterItem child in current.Items)
{
DoSomething(child);
}
}
结论
NestedRepeater
在处理递归数据方面无疑填补了一个空白。.NET 2.0 引入了一个新的接口和一组新类来处理分层数据。正如您在 MSDN 中所看到的,与处理其他 .NET 控件相比,它们的使用并不那么直接。这就是为什么即使使用 .NET 2.0,您也可能对 NestedRepeater
感兴趣,尤其是在您的数据易于处理时。
我在这里使用的示例非常简单,因为我不想添加不必要的内容,只想专注于控件本身。在下一篇文章中,我将展示 NestedRepeater
如何仅用几行代码就能构建优雅的通用 TreeView
控件。