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.

RendererGridView
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>

RendererInfinitiesLoop 

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;
}

RendererSwap

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);
    }
}

9 Comments

  • Very nice!
    This made my day.
    I was going to try to write the control this morning, but I had a meeting. &nbsp;Then I get back to my desk and you've already written it!

  • Based on what you did, I went ahead and wrote a GridViewRowExtender that inserts a row into a GridView at a given position (BeforeHeader, AfterFooter, etc) after the GridView databinds, and it's working great.

    I also confirmed another great use of the "render out of place" concept:

    Adding a custom footer for *inserting a new record*.

    I've never really had the need, but we just had a request to do it. There are tons of funky hacks out there for adding a "new" row, but most of them involve a lot of code and/or creating a bogus record in a DataTable, and none of them seem to work well with DataSourceControls backed by entities. Even if one manages to add such a row, it's difficult to ExtractRowValues and pass them to the Insert method of the DataSourceControl.

    In the past, I've always thought to myself that there's no functional difference between an "Insert" row and having a FormView on the same page and using that to insert new records. The only real difference is that sometimes it's a better use of screen real estate to *render* the form fields as the last row of the grid.

    So, with the "renderer" concept, I have added a FormView in Insert mode, bound to the same DataSourceID as the GridView. The FormView contains a GridViewRowExtender that creates a new row at the bottom of the grid and pipes the form elements into it.

    I'm still going to investigate whether it's possible to extend GridView and add Insert logic to it, but for now this works great and it's 100% declarative.

  • That pretty neat! And, the code is really short.

  • I found this article a few months ago and have been using the renderer control extensively in a number of different situations. So thanks for a great control.
    I am having a problem now however that is not unlike the your 1st example above involving a gridview, dropdownload and 2 renderers. The main variation is my dropdownlist has autopostback set. when postback occurs i get the good ol'
    Failed to load viewstate. &nbsp;The control tree into which viewstate is being loaded must match the control tree that was used to save viewstate ...
    Anyhow, wondering if you have run into this issue?
    Cheers &nbsp;David

  • David -- have you tried removing the renderers? I suspect you'll still get the error. The renderers dont do anything special prior to render, so the problem is probably elsewhere on the page.

  • Fantastic artile!

    This may be used to implement my WebForm Layout Manager.

    Ricky

  • This is fantastic - thank you. I am using this now to re-order controls inside of an UpdatePanel.

  • Apologies for VB, its an artefact of the environment I'm in.

    This is what I knocked up to move where a control is rendered without affecting the logical control hierarchy. Not sure why I inherited placeholder. *shrug*

    Private Class RenderProxy
    Inherits PlaceHolder
    Private _proxy As Control

    Public Sub New(ByVal proxy As Control)
    _proxy = proxy
    End Sub

    Public Overrides Sub RenderControl(ByVal writer As HtmlTextWriter)
    _proxy.RenderControl(writer)
    _proxy.SetRenderMethodDelegate(AddressOf NullRender)
    End Sub

    Private Sub NullRender(ByVal output As HtmlTextWriter, ByVal container As Control)
    End Sub
    End Class

  • Four years after, this code still kicks butt !!!

    Many thanks! This is exactly what I was looking for.

Comments have been disabled for this content.