Follow up
Rex M provided the answer but just wanted to follow up on what I've also found for posterity.
It seems like you can do either:
<ui:Tabs runat="server">
<ui:Tab TabText="Blah">
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/More.aspx" />
<ui:Node Url="~/Another.aspx" />
</ui:Tab>
<ui:Tab TabText="Nanner">
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/More.aspx" />
<ui:Node Url="~/Another.aspx" />
</ui:Tab>
<ui:Tab TabText="High There">
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/More.aspx" />
<ui:Node Url="~/Another.aspx" />
</ui:Tab>
</ui:Tabs>
OR
<ui:Tabs runat="server">
<TabItems>
<ui:Tab InnerHeading="Big Huge Heading" TabText="Big">
<NodeItems>
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
</NodeItems>
</ui:Tab>
</TabItems>
<TabItems>
<ui:Tab InnerHeading="Hi ya" TabText="Hi">
<NodeItems>
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
<ui:Node Url="~/Default.aspx" />
</NodeItems>
</ui:Tab>
</TabItems>
</ui:Tabs>
Then within code:
namespace Controls
{
[ToolboxData("<{0}:Tabs runat=server></{0}:Tabs>"), ParseChildren(true, "TabItems")]
public class Tabs : BaseControl, INamingContainer
{
private TabCollection tabItems;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
public TabCollection TabItems
{
get
{
if (tabItems == null)
{
tabItems = new TabCollection();
}
return tabItems;
}
}
protected override void Render(HtmlTextWriter writer)
{
writer.Write("<div id=\"tabs\" class=\"pane\" style=\"display: none\"><ul>");
int tabNumber = 1;
foreach (Tab tab in TabItems)
{
string li = string.Format("<li id=\"tab_{0}\"><a href=\"#tab{0}\"><span>{1}</span></a></li>", tabNumber, tab.TabText);
tabNumber++;
writer.Write(li);
}
writer.Write("</ul>");
tabNumber = 1;
foreach (Tab tab in TabItems)
{
string div = string.Format("<div id=\"tab{0}\" class=\"pane\"><h1>{1}</h1>", tabNumber, tab.InnerHeading);
tabNumber++;
writer.Write(div);
foreach (Node node in tab.NodeItems)
{
string a = string.Format("<a href='{0}'>{1}</a>", node.Url, "Text holder");
writer.Write(a);
}
writer.Write("</div>");
}
writer.Write("</div>");
}
}
public class TabCollection : List<Tab> { }
[ParseChildren(true, "NodeItems")]
public class Tab
{
private string tabText = string.Empty;
private string innerHeading = string.Empty;
private string showOn = string.Empty;
public string TabText
{
get { return tabText; }
set { tabText = value; }
}
public string InnerHeading
{
get { return innerHeading; }
set { innerHeading = value; }
}
public string ShowOn
{
get { return showOn; }
set { showOn = value; }
}
private NodeCollection nodeItems;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
public NodeCollection NodeItems
{
get
{
if (nodeItems == null)
{
nodeItems = new NodeCollection();
}
return nodeItems;
}
}
}
public class NodeCollection : List<Node> { }
public class Node
{
private string url = string.Empty;
public string Url
{
get { return url; }
set { url = value; }
}
}
}
This will obviously be changing (mine's going to be reading from the web.sitemap among other changes), but this should get anyone with the same needs well under their way.