Rendering ASP.NET Controls out of order

I receive a lot of comments and emails stemming from my series of articles on understanding the nuances of dealing with dynamic controls. It's interesting that so many of these requests revolve around very similar problems.

One such theme is where you would like to shuffle the controls on the page around a little bit. In a portal application like dotnetnuke, for example, the order modules are rendered on the page depends on custom configuration.

You can run into the nuances of dynamic controls if you plan on physically moving controls from one part of the control tree to another. For one, moving a control can cause it's unique ID to change. Normally that's ok, but sometimes something has already referenced that ID, and it's going to broken. Depending on when you move the control, you might be preventing postback data from loading correctly.

Not that there are no valid scenarios where you would want to move a control from one place to another. If you must, do so as early as possible in page lifecycle.

Or best of all -- avoid doing it in the first place.

In all of the comments/emails I received about this particular issue, the developer was making an assumption that doesn't have to be true. The assumption was that controls always render in the order they are in the tree, therefore, to change their order you must change the control tree.

Well here's a really simple control that shows that this is not true -- the Ordered Control:

namespace i88.Controls {
    using System;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Collections;
 
    public enum RenderOrder {
        Normal,
        Reverse,
        Random
    }
 
    public class Ordered : Control {
 
        public RenderOrder Order {
            get {
                object o = ViewState["Order"];
                if (o == null) {
                    return RenderOrder.Normal;
                }
                return (RenderOrder)o;
            }
            set {
                ViewState["Order"] = value;
            }
        }
 
        protected override void RenderChildren(HtmlTextWriter writer) {
            switch (Order) {
                case RenderOrder.Normal:
                    base.RenderChildren(writer);
                    break;
                case RenderOrder.Reverse:
                    RenderReverse(writer);
                    break;
                case RenderOrder.Random:
                    RenderRandom(writer);
                    break;
            }
        }
 
        private void RenderRandom(HtmlTextWriter writer) {
            ArrayList controls = new ArrayList(Controls);
            Random r = new Random();
 
            while (controls.Count > 0) {
                int nextIndex = r.Next(0, controls.Count);
                Control nextControl = (Control)controls[nextIndex];
                nextControl.RenderControl(writer);
                controls.Remove(nextControl);
            }
        }
 
        private void RenderReverse(HtmlTextWriter writer) {
            for (int i = Controls.Count - 1; i >= 0; i--) {
                Controls[i].RenderControl(writer);
            }
        }
    }
}

Ok, "Ordered" is a terrible name. I'm not good at names. Maybe it should be called the Unordered control, but whatever.

The point of the control is to allow you to change how it's children are ordered. Here's an example:

<i88:Ordered runat="server" Order="Reverse">
    <asp:TextBox ID="TextBox1" runat="server" Text="TextBox 1" />
    <asp:TextBox ID="TextBox2" runat="server" Text="TextBox 2" />
    <asp:TextBox ID="TextBox3" runat="server" Text="TextBox 3" />
    <asp:TextBox ID="TextBox4" runat="server" Text="TextBox 4" />
    <asp:TextBox ID="TextBox5" runat="server" Text="TextBox 5" />
    <asp:TextBox ID="TextBox6" runat="server" Text="TextBox 6" />
</i88:Ordered>

The controls are declared 1, 2, 3, but I've put the control in 'Reverse' mode, so they actually appear on the page in the reverse order. It also supports "Random".

But one of the key features of this control that helps drive home a point is that you can change the ordering mode on a postback, dynamically. The controls will happily rearrange themselves. No postback issues. No ViewState issues. No problems at all.

I haven't taken a look at dotnetnuke for a long, long time. But a long time ago when I did look at it, I noticed that it orders modules it contains by physically adding them to the page in the right order. When you change the ordering of modules on a page, it causes a postback. During the postback, the configuration file is changed, and then a Response.Redirect causes the page to reload from scratch.

Using this technique, that redirect wouldn't be necessary. Just render them in the new order.

Put this thing inside an ASP.NET AJAX Update Panel and put it in "Random" mode. Kind of fun to play with.

Enjoy.

PS: Published using the latest Live Writer Beta 3. Sweet.

9 Comments

  • Genious.

    No, seriously. Very very clever. Congrats.

  • This is the same kind of technique I've been using to randomize the list of links at http://SharpToolbox.com/links
    Really useful indeed to randomize an inline list without having to externalize it.

    The difference is that I don't use a control but a helper method that looks like this:

    static readonly Random _Random = new Random();

    ///
    /// Randomizes a collection of controls.
    ///
    static public void RandomizeControls(ControlCollection controls, bool removeLiteralControls)
    {
    List temp;

    temp = new System.Collections.Generic.List(controls.Count);
    for (int i = controls.Count; i > 0; i--)
    {
    int index = _Random.Next(i);
    Control control = controls[index];
    if (!removeLiteralControls || !(control is LiteralControl))
    temp.Add(control);
    controls.RemoveAt(index);
    }

    foreach (Control control in temp)
    controls.Add(control);
    }

    I use it this way: Util.RandomizeControls(LinkList.Controls, true)

    where LinkList is a control defined like this:


    SomeSite
    AnotherSite
    ...

  • Fabrice -- cool, but it does what I was trying to avoid. All you really care about is what order they are rendered in. You don't care what order they are in the control tree in, right? Removing and Adding controls does a lot more than just change their order. It can and will break controls depending on the exact scenario and the control being moved. But just altering the order they are rendered to the HtmlTextWriter is never going to cause a problem.

  • It makes sense for rendering the children of a control in random order, but it sounds like the modules would be better off being positioned in the page using css.

  • I have a related question that I'm just going to throw out there. I think it's a valid scenario. Maybe someone will have some ideas.

    Our users often ask us to provide functionality like that of Excel's Autofilter.

    This is pretty easy to accomplish by using a GridView, a DataSource (NHibernateDataSource in our case), and ControlParameters bound to DropDown controls on the page.

    This works great when we put the DropDowns *above* the GridView, but I can't figure out a good way to put them into the *header* of the GridView (excel-style), due to the way GridView does DataBinding and manages its child controls.

    It seems to me that this is just a rendering issue. I've been trying to figure out a way to pipe the rendered output of the DropDown controls into the GridView header without making the DropDown controls themselves children of the GridView in the control tree.

    Sound crazy? Any ideas?

  • Winston -- is there a reason why you don't want to make them children to the gridview? You should be able to do this using the HeaderTemplate of each field.

    If that's not enough perhaps you could actually move the elements on the client side. You could even write an extender to do it -- you attach the extender to a control and specify a gridview ID and column index, and client side it physically moves the dom element to inside the grid view. That would be interesting to say the least :) But just putting it on the header template would be a lot more straightforward.

  • Thats a pretty good explaination of the issues :) I wish I had a simple solution for you, but I don't. Your RenderPipe idea is interesting... you could even code it so you can hook it up declaratively -- two RenderPipe controls pointing at one another. If the source happens to be after the target, well then the target forces the source to render by calling render directly on it, then source avoids rendering when its turn finally comes. Insane? Maybe... I don't know what such a control would be good for except this exact scenario. But it might be fun to write...

    Oh and about the jumping of controls with an extender -- you could render invisible at first (display:none), so you couldn't see the thing jump from one place to another. But you still might see them pop into existence in the header, which isn't ideal but better. But perhaps you could then take it a step further by rendering empty placeholder dropdowns in the header which are then replaced. You'd never notice the swap then.

  • Winston -- thinking about it, it shouldn't be too bad. You put the dropdowns inside the header. Override OnLoad -- look for a key in ViewState that contains the current filter data, and you bind the grid with it (may be none in which case you'd be binding with no filter). When a dropdown selection is made it causes a postback. You store the new filter in the viewstate key you're checking for in LoadViewState and rebind with it.

    The trick is storing the filter data in ViewState separately from the GridView, so you can get the filter without having to bind it first.

  • make sure not to have any inline code (like in your aspx or ascx file to make this work!

Comments have been disabled for this content.