How To Get Commas In A List Using A Repeater
One really annoying problem I ran into when updating my blog was the way the ASP.NET Repeater rendered HTML with literal white space included in the template. I had a list of strings, each one a link, that were separated by commas. The rendering of the ItemTemplate included extra space added to the output, so I would get something like this: string1 , string2, but what I really wanted was string1, string2. In reality this is a browser problem, but since both major browsers I tested it on had the same issue, I had to find a solution.
A little time with google and I ran into this post which describes the problem.
There were two obvious solutions, the first was to manually layout the Repeater ItemTemplate in such a way that there was limited white space by defining the <ItemTemplate>code</ItemTemplate> all on one line. The big problem here was that if the code ever got reformatted things would break. The second solution was to override the rendering of the ItemTemplate. This turns out to be really tricky, since there really isn't a render method for the Repeater, the child objects do the work, and the child objects can't be easily manipulated.
This is my Repeater definition, the binding to TagSeparater (see below) looks at the items collections and generates the necessary separator (i.e. a comma). This Repeater is nested in a outer Repeater, thus the direction specification of the DataSource attribute.
<asp:Repeater ID="TagCtrl" runat="server" DataSource="<%# ((Blog.DataModel.NavigationPost)Container.DataItem).Tags %>"
Visible="<%# ((Blog.DataModel.NavigationPost)Container.DataItem).Tags.Count != 0 %>"
OnPreRender="Repeater_OnPreRender">
<HeaderTemplate>
tagged with
</HeaderTemplate>
<ItemTemplate>
<%# TagSeparator(Container)%><a href="<%# Eval("TagValue", "/PostSummariesByTag/{0}") %>" rel="tag"><%# Eval("TagValue") %></a>
</ItemTemplate>
</asp:Repeater>
The generated code contains the calls to the DataBoundLiteralControl:
Line 507: [System.Diagnostics.DebuggerNonUserCodeAttribute()]
Line 508: private global::System.Web.UI.DataBoundLiteralControl @__BuildControl__control11() {
Line 509: global::System.Web.UI.DataBoundLiteralControl @__ctrl;
Line 510:
Line 511: #line 100 "C:\Dev\Projects\ScottBlog2\WebSite\HemingwayTheme\PostSummaries.aspx"
Line 512: @__ctrl = new global::System.Web.UI.DataBoundLiteralControl(4, 3);
Line 513:
Line 514: #line default
Line 515: #line hidden
Line 516: @__ctrl.TemplateControl = this;
Line 517: @__ctrl.SetStaticString(0, "\r\n ");
Line 518: @__ctrl.SetStaticString(1, "<a href=\'");
Line 519: @__ctrl.SetStaticString(2, "\' rel=\"tag\">");
Line 520: @__ctrl.SetStaticString(3, "</a>\r\n ");
Line 521:
Line 522: #line 100 "C:\Dev\Projects\ScottBlog2\WebSite\HemingwayTheme\PostSummaries.aspx"
Line 523: @__ctrl.DataBinding += new System.EventHandler(this.@__DataBind__control11);
Line 524:
Line 525: #line default
Line 526: #line hidden
Line 527: return @__ctrl;
Line 528: }
On lines 517 and 520 you can see the extra newlines ("\r\n"), that's what I want to get rid of. Unfortunately, the DataBoundLiteralControl doesn't have methods to access the strings. Using Reflector I managed to get the name of the private field that contained the strings I needed to changed. I wrote a small bit of reflection code to get at the fields and attached it to the PreRender event:
protected virtual void Repeater_OnPreRender(object sender, EventArgs e)
{
Repeater repeater = sender as Repeater;
for (int j = 0; j < repeater.Items.Count; j++)
{
RepeaterItem item = repeater.Items[j];
for (int i = 0; i < item.Controls.Count; i++)
{
DataBoundLiteralControl db = item.Controls[i] as DataBoundLiteralControl;
if (db == null)
{
continue;
}
string[] strings = GetPrivateStrings(db);
if (strings == null)
{
continue;
}
for (int sn = 0; sn < strings.Length; sn++)
{
db.SetStaticString(sn, strings[sn].Replace("\r\n", "").Trim());
}
}
}
}
This code uses reflection to get at the method I need access to.
protected virtual string[] GetPrivateStrings(DataBoundLiteralControl control)
{
BindingFlags TooPrivate = System.Reflection.BindingFlags.NonPublic
| BindingFlags.Instance
| BindingFlags.DeclaredOnly;
FieldInfo staticLiteralsField = typeof(DataBoundLiteralControl)
.GetField("_staticLiterals", TooPrivate);
if (staticLiteralField == null)
return null;
string[] staticLiterals = (string[])staticLiteralsField.GetValue(control);
return staticLiterals;
}
The TagSeparator method looks like this:
protected virtual string TagSeparator(object container)
{
RepeaterItem item = container as RepeaterItem;
return MakeSeparator(item.ItemIndex, ((List<Blog.DataModel.NavigationPost>)PostsRepeater.DataSource)[0].Tags.Count);
}
private string MakeSeparator(int index, int count)
{
if (index > 0)
{
if (index == (count - 1))
{
return " and ";
}
return ", ";
}
return string.Empty;
}
Granted this is still a long way from being an ideal solution (which would probably be to write a bunch of C# code to replace the ItemTemplate, or perhaps a new subclass of Repeater); but, it works, and Microsoft doesn't really update the ASP.NET framework very often. In the worse case the internals of DataBoundLiteralControl will change and this will no longer be able to change the strings, but it will not fault and the unwanted spaces will reappear and I'll fix it again!
There are no comments.