Nested Repeaters in ASP.NET - c#

I have a class that contains hierarchical data. I want to present this data in my ASP.net webapp using nested repeaters. How do I do this? I've only ever done one level of nesting, how do I do say five levels?
Each item can have zero or many sub items. I'm basically just indenting at each subleveling using some css stuff. I do not want to use the treeview control, I want to strictly stick with a repeater.
Update:
My data comes from a database. I have an item datatable with some basic properties.
Item
{
ID,
Name,
Description,
...
}
Then I have a many to many table with:
Parent
{
ParentID,
ChildID
}
I'm iterating through each item and displaying its children; and its children's children. I assume this would best be accomplished with nested repeaters, but I could be wrong.

I've found that the simplest way to do nested repeaters without worrying about databinding events is to just set the DataSource using <%# %> syntax.
For example:
<asp:Repeater runat="server" id="Departments">
<ItemTemplate>
Name: <%# Eval("DeptName") %>
Employees:
<asp:Repeater runat="server" DataSource='<%# Eval("Employees") %>'>
<ItemTemplate><%# Eval("Name") %></ItemTemplate>
<SeparatorTemplate>,</SeparatorTemplate>
</asp:Repeater>
</ItemTemplate>
</asp:Repeater>
This is presuming that your Departments class has an Employees property - eg:
public class Department {
public string DeptName {get; set;}
public IEnumerable<Employee> Employees {get; set;}
}
public class Employee {
public string Name {get; set;}
}
If your outer-repeater object doesn't have a property corresponding to the inner-repeater object you can still use this trick, by adding a method in your code-behind that does the calculation. So your inner repeater might become:
<asp:Repeater runat="server" DataSource='<%# GetEmployees(Container.DataItem) %>'>
and then GetEmployees might look something like:
protected IEnumerable<Employee> GetEmployees(object item) {
var dept = (Department) item;
// then do whatever is necessary to get the employees from dept
return employees;
}

It's always cleaner to deal with the datasource than messing about with ItemDataBound, but this is even more the case when nesting Repeaters:
<asp:Repeater DataSource="<%#ColOfCol%>" runat="server">
<ItemTemplate>
<tr>
<asp:Repeater DataSource="<%#Container.DataItem%>" runat="server">
<ItemTemplate>
<td><%#SomeExtractingMethodLikeEval()%></td>
</ItemTemplate>
</asp:Repeater>
</tr>
</ItemTemplate>
</asp:Repeater>
The inner datasource could also be an evaluated property, or a call to a method that returns the enumeration wanted. Just be aware that it will be called with an object. I prefer to write the specific version, and then overload:
protected IEnumerable<string> GetNames(Family fam)
{
foreach(Person p in fam.Members)
yield return p.FirstName + " " + p.Surname;
}
protected IEnumerable<string> GetNames(object famObj)
{
return GetNames((Family)famObj);
}
One thing to be aware of is that if you want to get the current object in the parent repeater than you have to obtain it with:
((RepeaterItem)Container.Parent.Parent).DataItem

You can nest repeaters without a problem. More then 2 levels deep gets nasty though. Here's how:
The html looks something like this:
<asp:Repeater ID="r1" runat="server" OnItemDataBound="r1_ItemDataBound">
<ItemTemplate>
<!-- top level repeater element template here -->
<asp:Repeater ID="r2" runat="server" onitemdatabound="r2_ItemDataBound">
<ItemTemplate>
<!-- child repeater element template here -->
</ItemTemplate>
</asp:Repeater>
</ItemTemplate>
</asp:Repeater>
The codebehind looks like this:
protected void r1_ItemDataBound(object sender, RepeaterItemEventArgs e) {
Repeater r2 = (Repeater)e.Item.FindControl("r2");
r2.DataSource = yourDataSourceHere; // you'll have to query for appropriate data
r2.DataBind();
}
protected void r2_ItemDataBound(object sender, RepeaterItemEventArgs e) {
// do the same thing here for the 3rd nested repeater if you have a third, and so on
}

<asp:Repeater ID="R1" runat="server">
<ItemTemplate>
<asp:Repeater ID="R2" runat="server">
</asp:Repeater>
</ItemTemplate>
</asp:Repeater>
R1.ItemDataBound += (s, e) =>
{
var r2 = e.Item.FindControl("R2") as Repeater;
r2.DataSource = something;
r2.DataBind();
};
Be aware that FindControl is not recursive, it will only get the children.

Related

Appending to div with specific class

How can I append some html to a div with a specific class name?
frontend:
<div class="school-team-popup"></div>
backend:
StringBuilder _html = new StringBuilder();
_html.AppendFormat("<li>Hi there</li>");
I would like to append _html inside the school-team-popup div. How can I do this from the backend?
I'm going to explain the Web Forms way of doing things.
If you have some static markup that you want to selectively show/hide on your page, that's generally accomplished by setting the Visible property of the control.
<%-- This is in your ASPX markup (.aspx) --%>
<asp:Panel runat="server" id="HelloWorldPanel">
<p>Hello, world!</p>
</asp:Panel>
//This is in your code behind (.aspx.cs)
//hide the panel
HelloWorldPanel.Visible = false;
//show the panel
HelloWorldPanel.Visible = true;
If you're trying to grab dynamic data from some other source and display it on the page, you would declare the markup on your page to show this data, and then bind the data to the markup. There are many controls you can bind data to.
One example of a control you can bind data to is a repeater. Repeaters are good when you want tight control over the markup that gets rendered on the page. You bind them to some enumerable object such as a List<T> and then it will repeat some markup for each element in the enumerable object.
//This is in your project somewhere
namespace MyNamespace
{
public class Product
{
public int Id { get; set; }
public int Name { get; set; }
}
}
<%-- This is in your ASPX markup (.aspx) --%>
<ul>
<asp:Repeater runat="server" id="ProductRepeater" ItemType="MyNamespace.Product">
<ItemTemplate>
<li><%#: Item.Id %> - <%#: Item.Name %></li>
</ItemTemplate>
</asp:Repeater>
</ul>
//this is in your code behind (.aspx.cs)
protected void Page_Load(object sender, EventArgs e)
{
if(!IsPostback)
{
List<Product> products = MyDataLayer.GetProducts();
ProductRepeater.DataSource = products;
ProductRepeater.DataBind();
}
}

Repeater datasource Connection not working

Here is my Code Structure.
Repeater Markup:
<asp:Repeater runat="server" ID="RPMenu" DataSource='<%# Menues.GetAllMainMenu() %>'>
<ItemTemplate>
<%# Eval("MenuName") %><br />
<asp:Repeater runat="server" ID="RPMenuUnder" DataSource='<%# Menues.GetAllMainMenu(Convert.ToInt32(Eval("MenuID"))) %>'>
<ItemTemplate>
<%# Eval("MenuName") %><br />
</ItemTemplate>
</asp:Repeater>
</ItemTemplate>
</asp:Repeater>
Menu Class :
public static List<Menu> GetAllMainMenu(int parrentID = 0)
{
using (Scooterfrøen_Entities db = new Scooterfrøen_Entities())
{
return db.Menu.Where(i => i.ParentMenuID == parrentID).ToList();
}
}
Database table:
MenuID | MenuName | MenuDescription | ParrentMenuID | MenuUrl
I have several rows where ParentMenuID IS 0 and NOT NULL.
But for some reason Repeater control does not list anything on the site.
What could be the reason why Repeater control don't show anything?
I think page still requires to bind even though you have provided datasource in markup.
Write Page.DataBind(); in your page load event:
protected void Page_Load(object sender, EventArgs e)
{
Page.DataBind();
}
How to Use nested Repeater:It seems you have to handle ParentRepeater_ItemDataBound event to bind child repeater control.
Checkout this article available here.
You have to set values and bind repeater
RPMenu.DataSource=<value you want to set>;
RPMenu.DataBind();
A combination of the two existing answers is closest to true.
Rather than run Page.DataBind(), which may have unintended side-effects, run RPMenu.DataBind() in the Page_Load event handler. It will use the DataSource which is set in code-front.
The inner Repeater should automatically be bound. You only need to explicitly databind the outermost Repeater.

How to create capital letter row for a list of names in an ASP Repeater

I'm using an asp repeater to display a list of names and I want to display the current letter as a type of grouping header, like in an index page.
The data is sorted alphabetically before binding, but i'm finding it difficult to insert the 'A' and 'B' before the names are displayed.
Add a Panel control to your ItemTemplate with visibility set to False. When you are binding the repeater (assuming you are subscribing to the ItemDataBound event), run a check to see if the first letter has changed. If it has, set the panel's visibility to true and print out the letter.
Let me know if you need some example code.
EDIT: EXAMPLE CODE
For clarity sake, "AlphaHeaders" is what we will call the "A", "B", "C" letters that we want to display
aspx code
The Repeater will look like so:
<table>
<asp:Repeater id="rptRepeater" runat="server" OnItemDataBound="rptNames_OnItemDataBound">
<ItemTemplate>
<asp:Panel id="pnlAlphaHeader" runat="server" visible="False">
<tr><td><asp:Label id="lblAlphaHeader" runat="server" /></td></tr>
</asp:Panel>
<tr><td><asp:Label id="lblName" runat="server" /></td></tr>
</ItemTemplate>
</asp:Repeater>
</table>
aspx.cs code
First, you need a variable that holds the current AlphaHeader:
private string _AlphaHeaderCurrent = String.Empty;
Then you will do your work on the repeater's OnItemDataBound event:
protected void rptNames_OnItemDataBound(object sender, System.Web.UI.WebControls.RepeaterItemEventArgs e)
{
if ((e.ItemType==ListItemType.Item) || (e.ItemType==ListItemType.AlternatingItem)) {
string name = e.Item.DataItem("Name").ToString();
//check if the first letter of the current name is new. If it is new, we print out the header
if(!name.StartsWith(this._AlphaHeaderCurrent)) {
this._AlphaHeaderCurrent = name.SubString(1);
((Panel)e.ItemFindControl("pnlAlphaHeader")).Visible = true;
((Label)e.Item.FindControl("lblAlphaHeader")).Text = the._AlphaHeader;
}
((Label)e.Item.FindControl("lblName")).Text = name;
}
}
You sort before you bind.
That is, bind the sorted result set.
Without seeing the values you have, however, it is not possible to tell exactly how to do so.
Update - from your comment, I would say you need to change your binding source to something like Dictionary<string,IList<string>>.
With such a structure, you could bind by key (sorted) and sublist (secondary sort).
You need to sort your data before bind it to the repeater.
You can use nested repeaters (repeater inside repeater). Like category and subcategory.
In first repeater you can list all your names and make a condition starts with A. Then in sub repeater you can show all names. You will also use itemdatabound event to bind second repeater.
<asp:Repeater id="rptFirstLetters" runat="server" OnItemDataBound="rptChars_OnItemDataBound">
<ItemTemplate>
<div>"<%#Eval("letters") %>"
<asp:Repeater id="rptNames" runat="server">
<ItemTemplate>
<%#Eval("names") %>
</ItemTemplate>
</asp:Repeater>
</div> // bind all letters
</ItemTemplate>
Not really a nice way of doing this to be honest, repeaters generally result in ugly code I've found. The hierarchical approach from kad1r is probably the nicest if you can set it up, but there are alternatives, depending on your implementation details; I kind of prefer this in some ways as it keeps the markup very clean, and as I have a non-programmer design guy that is a plus for me.
ASPX:
<%# Page language="C#" Src="test.CODE.cs" Inherits="test_Page" %>
<asp:Repeater ID="TestRepeater" runat="server">
<ItemTemplate>
<asp:PlaceHolder Visible='<%# Eval("IsFirstInGroup") %>' runat="server">
<strong><%# Eval("Initial") %></strong><br/>
</asp:PlaceHolder>
<%# Eval("Name") %><br/>
</ItemTemplate>
</asp:Repeater>
CODE BEHIND:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public class test_Page : Page
{
protected Repeater TestRepeater;
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
String[] names = new String[]
{
"Alpha, John",
"Altman, Mary",
"Asher, Cyril",
"Bachman, Turner",
"Beta, Rob",
"Bexman, Norah",
"Clark, Freddy"
};
List<_DispItem> l = new List<_DispItem>();
for (int i = 0; i < names.Length; i++)
l.Add(new _DispItem() { Name = names[i], IsFirstInGroup = (i == 0 || names[i - 1][0] != names[i][0]) });
TestRepeater.DataSource = l;
TestRepeater.DataBind();
}
private class _DispItem
{
public String Name { get; set; }
public String Initial { get { return Name.Substring(0, 1); } }
public bool IsFirstInGroup { get; set; }
}
}

Anonymous type for nested repeater (ASP .NET)

I have two nested repeaters in my *.aspx page.
<asp:Repeater runat="server" id="rptMain">
<ItemTemplate>
<h1><%#DataBinder.Eval(Container.DataItem, "Name")%></h1>
<asp:Repeater runat="server" DataSource='<%# getUser(Convert.ToInt32(DataBinder.Eval(Container.DataItem, "FieldKey"))) %>'>
<HeaderTemplate><ol></HeaderTemplate>
<ItemTemplate>
<li class="<%#DataBinder.Eval(Container.DataItem, "CSSStyle")%>" id="li<%#DataBinder.Eval(Container.DataItem, "FieldKey")%>">
<%#DataBinder.Eval(Container.DataItem, "NameSubject")%>
</li>
</ItemTemplate>
<FooterTemplate></ol></FooterTemplate>
</asp:Repeater>
</ItemTemplate>
</asp:Repeater>
getUser is a protected method. It must returns the List with the following properties:
CSSClass
FieldKey
NameSubject
But CSSClass property is defined into the anonymous type.
protected List<????> getUser(int id)
{
DataClassesDataContext datacontext = new DataClassesDataContext();
var t1= from t in datacontext.GetAllCustomSubject(id).ToList()
select new { t.NameSubject, t.FieldKey, CSSStyle = t.IsDeleted ? "deleted hidden" : "real visible" };
return t;
}
How can I return such List? What kind of datatype can I Use instead of ???? ?
In general, can I use nested repeaters with anonymous types?
Well, the repeater won't care, I suspect - so just declare it to return IEnumerable.

ASP.NET: Access DataItem from an Event Handler

I have a data-bound, templated control, and inside the templated area I create a control with an event handler, like so:
<tnl:DisplayTree ID="DisplayTree1" runat="server" KeyPropertyName="Id"
ParentPropertyName="ParentDemographic" DataSourceID="DemographicObjectSource">
<ItemTemplate>
<asp:CheckBox ID="DemogSelector" runat="server" OnCheckedChanged="DemogSelector_OnCheckedChanged" />
<asp:Label ID="InlineEditLabel" runat="server" Text='<%#DataBinder.Eval(Container.DataItem, "Name") %>'></asp:Label>
</ItemTemplate>
</tnl:DisplayTree>
Within the event handler, I would like to be able to detect the Key of the item for which the control was created. For example:
protected void DemogSelector_OnCheckedChanged(object sender, EventArgs e)
{
CheckBox selector = (CheckBox)sender;
DisplayTree.TreeNode treeNode = (DisplayTree.TreeNode)selector.Parent.Parent.Parent.Parent;
Label1.Text += (int)treeNode.Key + ", ";
}
As you can see, this approach requires close knowledge of the hierarchy within my DisplayTree.TreeNode class (i.e. I have to know that sender.Parent.Parent.Parent.Parent is where I'll find the DisplayTree.TreeNode object). I would like to make it a bit more robust, so that if my TreeNode hierarchy changes or something, I can access the key without difficulty. What's the best way for me to make this Key available from within the event handler?
Best way is to add a custom attribute to your checkbox
<asp:CheckBox ID="DemogSelector" runat="server" oncheckedchanged="DemogSelector_CheckedChanged" AutoPostBack="true" key='<%# Eval("Id") %>'/>
And then access it using
string key = (sender as CheckBox).Attributes["key"];
One potential way is to add an extension method to the checkbox, which finds the TreeNode for you, it could be implemented as some loop which recursively searches the parents until the TreeNode is found.
That way you just call selector.FindTreeNode(), of course this would fail on any checkbox called outside of your structure.
The best way is to create your own event arguments class that inherits from EventArgs and expose it as a property there. Since you're using a custom or third-party control, I don't have any advice on how to set the property on the event args. A custom events args would look like this:
public class TreeNodeCheckedChangedEventArgs : EventArgs
{
TreeNodeCheckedChangedEventArgs(int nodeKey)
{
NodeKey = nodeKey;
}
int NodeKey { get; private set; }
}
You can set the value attribute on the checkbox
<asp:CheckBox ID="DemogSelector" runat="server" oncheckedchanged="DemogSelector_CheckedChanged" AutoPostBack="true" value='<%#DataBinder.Eval(Container.DataItem, "Id") %>' />
And retrieve server side
string value = ((CheckBox)sender).Attributes["value"];
I´m using kind of a similar solution as others, adding my code if it helps:
In markup:
<ItemTemplate>
<input type="checkbox"
value="<%# Container.DataItemIndex %>" name="CheckedItems" />
</ItemTemplate>
And in eventhandler:
string checkedItemRowIds = Request.Form["CheckedItems"];

Categories

Resources