65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET 嵌套 Repeater 控件

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.79/5 (9投票s)

2005年12月8日

6分钟阅读

viewsIcon

95658

downloadIcon

1688

一个服务器控件,其原理与 ASP.NET Repeater 类似,但可以处理递归(或分层)数据。

引言

每次我不得不使用 ASP.NET Repeater 控件处理递归数据时,我都会感到沮丧。没有简单的方法可以利用 Repeater 控件为这些数据提供数据绑定功能,例如,当我需要显示一个可能拥有多个“子”节点且具有任意数量子级别数据的树视图时。

当然,在简单的场景中,当只有两到三个数据级别时,我可以在我的 WebForm 上放置多个 Repeater,在数据源中创建适当的 DataRelations,这样工作就完成了。但是,如果我无法提前知道需要显示多少子级别的数据,这种解决方案就无法使用。即使可以,这种解决方案也不够优雅:所有这些 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 表中,有两个顶级节点:InvertebratesVertebrates

可选地,您可以使用 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 接口的类(这个类对我们是隐藏的),并且这个类的实例被赋给 NestedRepeaterItemTemplate 属性。这个类拥有 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

一旦项被添加到 NestedRepeaterControls 属性中,就会为每个子节点调用 CreateItem,并将级别增加一个。

由于我们使用的是递归函数,因此可以有任意多的数据级别:无需提前知道有多少子级别。也就是说:我们可以向 SQL 表添加另一个子级别,而无需更改或添加任何一行代码。

更新

NestedRepeaterNestedRepeaterItem 类中添加了一个名为 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 控件。

© . All rights reserved.