Rendering ASP.NET Controls out of place
Two posts back I discussed a technique you can use to render controls in an order other than how they are physically arranged in the control tree.
A reader, Winston Fassett, posted a comment asking for some advice on how to get a control to render into the header of a GridView. Specifically, he wanted a DropDownList to appear in the header of each column, where the value you select from the list filters the data to only that value. If you've used SharePoint, same idea.
Putting DropDownLists in the HeaderTemplate of a TemplateField is pretty straight forward and would work just fine. But Winston also wanted to have ViewState disabled on the GridView altogether, and that means binding it every request. But how do you bind it if you don't know what the filter is? And how do you know what the filter is if you haven't databound the gridview yet? The header template isn't instantiated until you databind the GridView, so you've got a chicken-and-the-egg problem.
There's a simple solution involving storing the current filter in the page's ViewState. But in the comments Winston had a rough idea for a control that would just allow him to move the rendering of a DropDownList to inside the GridView, but without having to physically put it into the GridView. He called his idea a RenderPipe or RenderCache control.
I got me thinking about just how easy it would be to implement and the interesting things you could do with it. So here you are... thanks to Winston, the Renderer control.
Terrible name, I know. Kind of hard to say. But it's descriptive at least! It's called the Renderer control because that's what it does -- you put it somewhere, point it at another Renderer control, and it renders the other control in its place.
With it, getting that DropDownList that is outside the GridView to inside the header template is easy.
<i88:Renderer id="RenderSource" runat="Server">
<asp:DropDownList ID="ddl" runat="server">
<asp:ListItem Text="Item 1" />
<asp:ListItem Text="Item 2" />
<asp:ListItem Text="Item 3" />
</asp:DropDownList>
</i88:Renderer>
<asp:GridView ID="gv1" runat="server">
<Columns>
<asp:TemplateField>
<HeaderTemplate>
<i88:Renderer SourceID="RenderSource" runat="server" />
</HeaderTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
This is going to be a terribly worded paragraph. There are two Renderers here. One wraps the content we want, the DropDownList, the other points at the first. The one doing the wrapping simply no-ops its Render method. The target Renderer, when it comes time to Render itself, finds the other Renderer and renders it instead of itself. Even if you put the Source Renderer after the target Renderer in the markup, it will work just fine, because they can communicate.
The DropDownList appears inside the GridView instead of where it actually is.
How many Renderers could a Renderer Render if a Renderer could Render Renderers?
How about, render a control a dynamic number of times?
<i88:Renderer id="RenderSource" runat="Server">
Infinities Loop
</i88:Renderer>
<asp:Repeater ID="rpt1" runat="server">
<ItemTemplate>
<i88:Renderer SourceID="RenderSource" runat="server" />
</ItemTemplate>
</asp:Repeater>
This is a useless example because, well, you could just put the control actually inside the repeater for the same effect. But with this, there's only one instance of the control which is repeated.
How about this -- what happens if you point two Renderers at each other? They swap positions! All without actually moving them within the control tree.
<table width="300px" border="1" cellpadding="1" cellspacing="1">
<tr>
<td align="center">
<i88:Renderer ID="r1" runat="server" SourceID="r2">
<h2>A</h2>
</i88:Renderer>
</td>
<td align="center">
<h2>B</h2>
</td>
<td align="center">
<i88:Renderer ID="r2" runat="server" SourceID="r1">
<h2>C</h2>
</i88:Renderer>
</td>
</tr>
</table>
<asp:Button runat="server" OnClick="Reverse" Text="Swap!" />
private void Reverse(object sender, EventArgs args) {
string r1Source = r1.SourceID;
r1.SourceID = r2.SourceID;
r2.SourceID = r1Source;
}
Terrible freehanding aside, this shows how you can not only dynamically determine the order controls are rendered, but you can actually change the structure of them, too! All without calling Controls.Remove.
Keep in mind that all this control does is call Render on the source. If you had two Renderers pointed at the same source, that control would be rendered twice on the same page. That might result in multiple elements with the same ID, among other problems.
UPDATE: Just realized another great general use this control has. Use it to put a control with ViewState enabled inside a control with ViewState disabled. Normally disabling ViewState on a control disables it for all its children as well, and there's no way to re-enable it for a control within. Now you can still have it appear to be within that control even though it isn't.
Oh yeah... here's the control.
public class Renderer : Control {
private bool _renderingSource = false;
public virtual string SourceID {
get {
return ((string)ViewState["SourceID"]) ?? String.Empty;
}
set {
ViewState["SourceID"] = value;
}
}
private Renderer FindSource() {
Control nc = NamingContainer;
Control c = null;
while (nc != null && c == null) {
c = nc.FindControl(SourceID);
nc = nc.NamingContainer;
}
if (c == null) {
throw new InvalidOperationException("Cannot find control with ID '" +
SourceID +
"'.");
}
Renderer source = c as Renderer;
if (source == null) {
throw new InvalidOperationException("Control with ID '" +
SourceID +
"'is not a Render control.");
}
return source;
}
protected override void Render(HtmlTextWriter writer) {
if (_renderingSource) {
base.Render(writer);
}
else if (!String.IsNullOrEmpty(SourceID)) {
RenderSourceControl(writer);
}
}
private void RenderControlAsSource(HtmlTextWriter writer) {
_renderingSource = true;
try {
RenderControl(writer);
}
finally {
_renderingSource = false;
}
}
private void RenderSourceControl(HtmlTextWriter writer) {
Renderer source = FindSource();
source.RenderControlAsSource(writer);
}
}