ASP.NET的NestedRepeater控件

分享于 

17分钟阅读

Web开发

  繁體 雙語

介绍

每次我必须使用 ASP.NET Repeater 控件来处理递归数据时,我感到沮丧。 如果没有一个节点可以使用 Repeater 控件提供的数据绑定功能,那么就没有简单的方法,例如当我必须显示一个节点,其中有一个节点可能含有一个未定义的数据子节点数。

当然,在一个简单的例子中,只有两个或者三个级别的数据,我可以将 Repeater 放在WebForm上,在数据源中创建足够的DataRelation,完成工作。 但是,如果我不能提前告诉我们数据的数据层次,这个解决方案是不可以能使用的。 即使我可以,这个解决方案也不会是优雅的: 所有这些 Repeater<ItemTemplate> 部分都是相同的。 有一个简单的方法声明一个控件,一个 <ItemTemplate> 部分与所有数据相同,并要求该控件按照当前节点不同。 这就是我决定提出一个新的控制的原因,我称之为 NestedRepeater

我希望这里控件支持声明性语法( 例如。 在我的to上声明一个 <ItemTemplate> 部分,并将所有控件放在它中,而不是强制使用后面的代码。 这里控件不需要预先知道必须处理多少次数据。

背景

你可以注意到,本文的"工作原理"部分假定你熟悉模板数据绑定控件( 有关详细信息,请参阅 MSDN )。

如何使用 NestedRepeater

作为本文的一个例子,我将使用 NestedRepeater 显示物种的动物分类。 我希望在这里分类中直观地呈现层次结构。

数据源

对于我打算显示的动物分类,数据来自下面的SQL table,称为 Animals:

可以有任意多个列,但这里所看到的三列是 NestedRepeater ( 注意,你可以为这些列提供任何想要的NAME,如下所示"databind"与其他控件一样,你也可以使用这些数据) 所必需的:

  • ANI_ID: 主键, 非空
  • ANI_NAME: 将显示的数据
  • ANI_PARENT: 引用 ANI_ID的外键

NestedRepeater 公开一个名为 DataSource的public 属性:

publicvirtual DataSet DataSource;

你可以看到,DataSource 是作为 DataSet 输入的,而不是作为 对象

你只需像往常一样执行数据绑定:

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的NAME
  • myRep.RowFilterTop 告诉 NestedRepeater 如何确定哪些记录将成为顶层节点。 这里,最上面的节点是列 ANI_PARENTnull的记录。 在我的SQL table 中,有两个最顶端的节点: InvertebratesVertebrates

可以选择使用 DataMamber 属性来指示 table的NAME。 如果没有,NestedRepeater 假定数据在 ds.Tables[0] 中可用。

然后:

myRep.DataSource = ds;
myRep.DataBind();

这里没有解释。

在WebForm上

要在web上使用 NestedRepeater,必须首先对程序集进行 register:

<%@Registertagprefix="meg"Namespace="WebCustomControls"Assembly="WebCustomControls"%>

如果使用 Visual Studio,则必须在项目中添加对 WebCustomControls.dll的引用,以便该行能够正常工作。

然后:

<meg:NestedRepeaterid=myRep runat="server"><HeaderTemplate> This is the animal classification. <br></HeaderTemplate><FooterTemplate> The end.
 </FooterTemplate><ItemTemplate><imgsrc="http://www.codeproject.com/pix.gif"height="10"width="<%# (Container.Depth * 10) %>"><%# (Container.DataItem as DataRow)["ANI_NAME"]%>
 <br></ItemTemplate></meg:NestedRepeater>

在这里,ContainerNestedRepeaterItem ( 这里类将在后面的"类型的工作原理,它提供了关于当前上下文的若干细节,这对于真正呈现层次视图非常重要。

比如 Container.Depth 告诉我们在层次结构中我们有多深。 在最顶层,Container.Depth 为 0. 在分段下方,为 1,然后为 2 等等,为了给出层次的感觉,我使用深度为宽度的( 不可见) 图像。 如果要按照当前分段对显示进行个性化,请检查这里属性。

Container.DataItem 总是作为 DataRow 输入,所以我们可以直接转换,而不是使用带有 DataBinder.Eval(...)的慢版本。 ( 请注意,我们必须导入 System.Data 命名空间。)

<HeaderTemplate><FooterTemplate> 部分的工作方式与 <asp:Repeater> 中的相同。 在本示例中,我不使用这里功能,但它们都支持数据绑定。

然后,结果应该如下所示:

使用指导

工作是在两个函数中完成的:

CreateControlHierachy

protectedvirtualvoid CreateControlHierarchy(bool createFromDataSource)
{
 int nbTopNodes = 0;
 DataView dv = null;
 // HeaderTemplateif (m_headerTemplate!= null)
 // Do we have a <HeaderTemplate> section? {
 NestedRepeaterHeaderFooter header = 
 new NestedRepeaterHeaderFooter();
 m_headerTemplate.InstantiateIn(header);
 if (createFromDataSource)
 header.DataBind();
 Controls.Add(header);
 }
 // ItemTemplateif (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)
 thrownew 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;
 elseif (i ==0)
 currentPos = NestedElementPosition.First;
 elseif (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;
 // FooterTemplateif (m_footerTemplate!= null)
 {
 NestedRepeaterHeaderFooter footer = 
 new NestedRepeaterHeaderFooter();
 m_footerTemplate.InstantiateIn(footer);
 if (createFromDataSource)
 footer.DataBind();
 Controls.Add(footer);
 }
 ChildControlsCreated = true;
}

这里函数在数据绑定(。或者,在 postback 上,在加载ViewState期间) 中调用。 它创建 header 并确定顶部节点的数目。 对于每个上面的节点,它调用 CreateItem。 最后,创建页脚。

trackpoint

privatevoid 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;
 elseif (i ==0)
 currentPos = NestedElementPosition.First;
 elseif (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。 它是用来支持该属性的变量:

publicvirtual ITemplate ItemTemplate
{
 get{return m_itemTemplate;};
 set{m_itemTemplate = value;};
}

ASP.NET 解析web服务时,<ItemTemplate> 部分是"已经转换"到实现 ITemplate 接口的类,这个类的实例受到 NestedRepeaterItemTemplate 属性影响。 这里类拥有 this inside 声明的所有控件。 在我的例子中,有一个 <> 标记和文本字符串。 在方法 InstanteIn 中( 由. NET),. NET 生成这两个控件并将它们添加到项目的Controls 属性中。 伪代码应如下所示:

// this code is generated by. NET. We don't see itvoid 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 :

publicenum 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 上面}

NbChildren 属性表示当前节点的直接子节点数。

Depth 属性已经提到,DataItem 属性与其他. NET 控件相同。 DataItem 属性始终是一个 DataRow

一旦将该项添加到 NestedRepeaterControls 属性中,则为每个子节点调用 CreateItem,并使用增量级别。

当我们使用递归函数时,可以有尽可能多的数据级别: 我们不需要提前知道我们有多少次子等级。 也就是:我们可以在 SQL table 中添加另一个分段,不需要更改或者添加任何单行代码。

更新

NestedRepeaterNestedRepeaterItem 类中添加了一个名为 Items的新属性。 这样可以编程方式循环遍历项,如下所示:

// a NestedRepeater called myRepeater has been declared elsewhere...foreach(NestedRepeaterItem item in myRepeater.Items)
{
 DoSomething(item);
}// DoSomething is a recursive functionprivatevoid DoSomething(NestedRepeaterItem current)
{
 // do whatever is required with the current item// * * * // then call DoSomething recursively for all sub-itemsforeach(NestedRepeaterItem child in current.Items)
 {
 DoSomething(child);
 }
}

结束语

在处理递归数据时,NestedRepeater 当然充满了空白。 .NET 2.0引入了一个新的接口和一组新类,可以解决分层数据。 就像你在中看到的,它们的用法并不像其他控件一样简单。 这就是为什么你可以能对 NestedRepeater 感兴趣,甚至在. NET 2.0中,特别是当你的数据很简单时。

我在这里使用的例子非常简单,因为我不想添加无用的东西,只关注于控件本身。 在下一篇文章中,我将展示 NestedRepeater 如何帮助构建优雅的通用 TreeView 控件,只需几行代码。


控制  asp  asp-net  NEST  Nested  
相关文章