September 2007 - Posts

This question comes up from time to time, to time. If you understand how redirects work, then you also know it is "not possible" to redirect into a new window, because a redirect on the server causes a special HTTP response to be sent to the users browser, the client. The browsers native implementation interprets the special response code and sends the user off to the destination. There's no built-in mechanism or standard for specifying a new window.

The only way to open a new window is for it to be initiated on the client side, whether it be through script or clicking on a link.

So the solution always proposed to this problem is to instead write out some script that opens the window, rather than using Response.Redirect:

<script type="text/javascript">
    window.open("foo.aspx");
</script>

Ok... so first you lecture me about how it is "not possible", and then you give me the code that makes it possible. Why can't I just redirect to a new window -- I don't care how HTTP works or client this or server that. There's obviously a solution, so why do I have to worry about it?

(The make-believe developers in my head are always quite temperamental)

It's easy enough to write a little helper that abstracts the details away from us... while we're at it, we might as well add 'target' and 'windowFeatures' parameters. If we're going to open the new window with script, why not let you use all of the window.open parameters? For example, with 'windowFeatures' you can specify whether the new window should have a menu bar, and what its width and height are.

public static class ResponseHelper {
    public static void Redirect(string url, string target, string windowFeatures) {
        HttpContext context = HttpContext.Current;
 
        if ((String.IsNullOrEmpty(target) ||
            target.Equals("_self", StringComparison.OrdinalIgnoreCase)) &&
            String.IsNullOrEmpty(windowFeatures)) {
 
            context.Response.Redirect(url);
        }
        else {
            Page page = (Page)context.Handler;
            if (page == null) {
                throw new InvalidOperationException(
                    "Cannot redirect to new window outside Page context.");
            }
            url = page.ResolveClientUrl(url);
 
            string script;
            if (!String.IsNullOrEmpty(windowFeatures)) {
                script = @"window.open(""{0}"", ""{1}"", ""{2}"");";
            }
            else {
                script = @"window.open(""{0}"", ""{1}"");";
            }
 
            script = String.Format(script, url, target, windowFeatures);
            ScriptManager.RegisterStartupScript(page,
                typeof(Page),
                "Redirect",
                script,
                true);
        }
    }
}

Now you just call ResponseHelper.Redirect, and it figures out how to honor your wishes. If you don't specify a target or you specify the target to be "_self", then you must mean to redirect within the current window, so a regular Response.Redirect occurs. If you specify a different target, like "_blank", or if you specify window features, then you want to redirect to a new window, and we write out the appropriate script.

One nice side effect of this "do you really need a new window?" detection is that it's dynamic. Say the destination you redirect to is configurable by some administrator. Now they can decide whether it opens in a new window or not. If they don't want it to they can specify blank or _self as the target.

Disclaimers:

Note: If you use it outside the context of a Page request, you can't redirect to a new window. The reason is the need to call the ResolveClientUrl method on Page, which I can't do if there is no Page. I could have just built my own version of that method, but it's more involved than you might think to do it right. So if you need to use this from an HttpHandler other than a Page, you are on your own.

Note: Beware of popup blockers.

Note: Obviously when you are redirecting to a new window, the current window will still be hanging around. Normally redirects abort the current request -- no further processing occurs. But for these redirects, processing continues, since we still have to serve the response for the current window (which also happens to contain the script to open the new window, so it is important that it completes).

Extension Methods

Recently, Eilon and Bertrand blogged about a novel use of some C# 3.0 features. Eilon posed the question, "Have you come up with a novel way to use a new language feature that you'd like to share?". Well here you go.

Extension Methods are a new feature in C# 3.0 (you'll need it for the rest of the article). They allow you to add methods to existing types, imported via a 'using' statement. I've seen a lot of debate over their use -- whether they are bad or good. Well -- I don't know, I don't really want to be involved in that debate. But I do know that in some scenarios they seem to fit perfectly. Like all language tools, you should use it sparingly and only when appropriate. I believe even the dreaded GOTO statement, which yes, exists in C#, has its place (I wasn't a believer originally, but some old coworkers of mine convinced me (Bob!)).

In this case, an extension method seems to work well. In general, whenever you find yourself writing a static Helper class whose only purpose in life is to help use the APIs of another type, it's probably a great candidate for extension methods. Especially if the first parameter to all those methods is the type you're trying to help with -- or if the methods always grabs the instance through some static API (like HttpContext.Current) or instantiates a new one.

By rewriting our ResponseHelper to use extension methods...

public static class ResponseHelper {
    public static void Redirect(this HttpResponse response,
        string url,
        string target,
        string windowFeatures) {
 
        if ((String.IsNullOrEmpty(target) ||
            target.Equals("_self", StringComparison.OrdinalIgnoreCase)) &&
            String.IsNullOrEmpty(windowFeatures)) {
 
            response.Redirect(url);
        }
        else {
            Page page = (Page)HttpContext.Current.Handler;
            if (page == null) {
                throw new InvalidOperationException(
                    "Cannot redirect to new window outside Page context.");
            }
            url = page.ResolveClientUrl(url);
 
            string script;
            if (!String.IsNullOrEmpty(windowFeatures)) {
                script = @"window.open(""{0}"", ""{1}"", ""{2}"");";
            }
            else {
                script = @"window.open(""{0}"", ""{1}"");";
            }
 
            script = String.Format(script, url, target, windowFeatures);
            ScriptManager.RegisterStartupScript(page,
                typeof(Page),
                "Redirect",
                script,
                true);
        }
    }
}

Note the 'this' keyword in the first parameter. Now whenever we include the namespace this class is defined within, we get a nice override on the actual Response object.

ResponseRedirect

Simply including a 'using' to a namespace is what gets extensions methods to show up. So it's probably a good idea to keep extension methods isolated to their own namespaces, lest someone get more than they bargained for when they use your namespace.

Also worth noting is that this is still a static API, so you can use it the traditional way, too. You just have to pass in the Response object as the first parameter.

And to see it in action...

Response.Redirect("popup.aspx", "_blank", "menubar=0,width=100,height=100");

Redirected into a new Window...

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

When you wrap content with an UpdatePanel, it pretty much takes care of everything for you. But it can't do absolutely everything...

Take for example some inline script:

<p>Some html before the script</p>
<script type="text/javascript">
    alert('hi');
</script>
<p>Some html after the script</p>

Inline meaning it appears inline with the rest of your HTML.

First of all -- I'd say it's best not to use inline script if you don't have to. If you can move that script block to the bottom or the top of the page, or to a separate javascript reference, without consequence, then why not keep the script and UI nice and separate? But there are times when inline script is either necessary or just preferred. For example -- check out how you instantiate an instance of Silverlight on your page. That's inline script. It makes sense to keep the script where it is, since that's where you want your Silverlight app to be.

Then, along came UpdatePanel...

The over simplified way of explaining how UpdatePanel does its work on the client is through the innerHTML DOM property. When a delta is retrieved from the server, it finds itself in the existing DOM, disposes of the contents, and then assigns the new content via innerHTML. Thanks to a basically consistent behavior across the major browsers, the new content appears just as if it were there to begin with.

But inline script doesn't work this way. Setting the innerHTML of a DOM element to HTML which contains a script block does not cause that script to execute. The only way to execute arbitrary script dynamically is to eval() it directly, or dynamically create a script element with document.createElement("script") and inject it into the DOM.

So if we had the above HTML+SCRIPT inside an UpdatePanel, whenever it updated, the inline script would simply be ignored.

UpdatePanel realizes that there may be script to be executed. But it only knows about such scripts if they are registered through the ScriptManager's Register script APIs. If you use that API correctly, UpdatePanel once again picks up the slack and takes care of the rest for you automatically.

ScriptManager.RegisterStartupScript(this, typeof(Page), UniqueID, "alert('hi')", true);

UpdatePanel intercepts registrations like these during asynchronous postbacks and sends the content down to the client separately from the HTML. And than the client side PageRequestManager dynamically injects a script element for you. So one solution to this inline script problem is simply not to use inline script. If you use this Register API all the time, it will work whether there is an update panel involved or not.

But I want my Inline Script back!

Alright already. So here is a novel little control that gives you the benefits of inline script without having the draw back of not working in an UpdatePanel.

public class InlineScript : Control {
    protected override void Render(HtmlTextWriter writer) {
        ScriptManager sm = ScriptManager.GetCurrent(Page);
        if (sm.IsInAsyncPostBack) {
            StringBuilder sb = new StringBuilder();
            base.Render(new HtmlTextWriter(new StringWriter(sb)));
            string script = sb.ToString();
            ScriptManager.RegisterStartupScript(this, typeof(InlineScript), UniqueID, script, false);
        }
        else {
            base.Render(writer);
        }
    }
}

If the request is normal, it just renders whatever the contents are as usual. If you happen to be in the middle of an asynchronous postback, it uses the RegisterStartupScript API.

<asp:UpdatePanel ID="up1" runat="server">
    <ContentTemplate>
        <i88:InlineScript runat="server">
            <script type="text/javascript">
                alert('hi');
            </script>
        </i88:InlineScript>
        <asp:Button ID="cmd" runat="server" Text="Update" />        
    </ContentTemplate>
</asp:UpdatePanel>

You get the beloved alert dialog when the update panel updates! Thankfully, because you still put the script element itself in the html, you still get javascript intellisense and all that jazz, too.

Simple, not terribly useful, probably not the best thing to do performance, but could be pretty handy in the right situation.

UPDATE: If you actually use this, you may want to add a check for sm == null as well as IsInAsyncPostBack, so that the control works even if there's no ScriptManager on the page.

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.

Part 1: Dynamic vs. Static
Part 2: Creating Dynamic Controls
Part 3: Adding Dynamic Controls to the Control Tree
Part 4: Because you don't know what to render at design time

UPDATE: Click here to download the Sample Code referenced in this article.

I started part 1 of this series so long ago. My original plan was to have all 4 parts done over four weeks. It's been a lot longer than that! Sorry folks!

The new plan is that four parts is not enough! I realized while writing this part that there's just too much to cover!

There are three main reasons I can think of why you would want to create controls dynamically instead of declaratively.

  1. Because the types and/or number of controls to be rendered is not known at design time. The form may be driven by a database or through configuration, or it is dynamically determined based on other data the user has provided.
  2. You know what controls may be rendered but there are a large number of possibilities and/or the controls are expensive to load, so you don't want to load them all.
  3. Because you are creating a custom server control and you don't have a choice.

In this part, we will cover #1 and #2. Part 5, hopefully the last part, will cover #3.

Example: You don't know "what" or "how many" controls should be rendered at design time

First and foremost -- ask yourself whether the problem is that you don't know "what" controls should be rendered, or if it's just "how many" controls should be rendered. If it's a question of how many, you can still solve the problem declaratively thanks to Templating.

This is a real scenario that someone emailed me about. They were attempting to solve the problem with dynamic controls. They were having various problems with their implementation and asked for my help. And of course, it turned out to be much simpler with templating.

The scenario is as follows:

You want users to be able to upload files to your site. They do this from a file manager page, where they can see a listing of the files that already exist. Next to each file is a button to delete the file. At the bottom of the list is a file upload control they can use to upload a new file. Any time a file is deleted or uploaded, the listing is updated.

To display the files, we will add rows dynamically to a table that is declared on the form. When a file is deleted we'll give some feedback via a label.

<asp:Label ID="lblStatus" runat="server" EnableViewState="false" />
<table id="tblFiles" runat="server" cellpadding="4" cellspacing="4" border="1"></table>

Here is the implementation I was given. In red, because it has problems!

public partial class _Default : Page {
    protected override void OnLoad(EventArgs e) {
        string[] files = Directory.GetFiles(Server.MapPath("~/uploaded"));
 
        foreach (string path in files) {
            // strips directory from path
            string fileName = Path.GetFileName(path);
 
            HtmlTableRow row = new HtmlTableRow();
            // file name cell
            HtmlTableCell cell = new HtmlTableCell();
            cell.InnerText = fileName;
            row.Cells.Add(cell);
 
            // delete cell
            LinkButton cmdDelete = new LinkButton();
            cmdDelete.Text = "delete";
            cmdDelete.CommandArgument = fileName;
            cmdDelete.Command += new CommandEventHandler(cmdDelete_Command);
            cell = new HtmlTableCell();
            cell.Controls.Add(cmdDelete);
            row.Cells.Add(cell);
 
            // add row to table
            this.tblFiles.Rows.Add(row);
        }
        base.OnLoad(e);
    }
 
    private void cmdDelete_Command(object sender, CommandEventArgs e) {
        // command argument contains the file name to delete
        string fileName = Path.GetFileName((string)e.CommandArgument);
        string path = Path.Combine(Server.MapPath("~/uploaded"), fileName);
 
        File.Delete(path);
        this.lblStatus.Text = "Deleted " + fileName;
 
        // now remove the table row for the file
        LinkButton cmdDelete = (LinkButton)sender;
        // link button's parent = cell, parent.parent = row
        HtmlTableRow row = (HtmlTableRow)cmdDelete.Parent.Parent;
        this.tblFiles.Rows.Remove(row);
    }
}

It seems pretty straight forward. The first cell shows the file name, the second shows a link we can click on to delete it. When deleting a file, we remove it from the file system and then remove the row from the table. And of course, we let the user know the file was deleted successfully. Let's see how it runs...

File Manager

Cool -- let's delete "InfinitiesLoop.gif", who wants that thing anyway?

Deleted InfinitiesLoop.jpg

So far so good! Now, let's delete "TheFamily.jpg". I was blinking during that picture, and I don't appreciate the bunny ears over my head, sister. Bah...

Deleted TheFamily.jpg

WHAT?! I swear I clicked delete on TheFamily.jpg, but it deleted my TODO list! Nooooo.....! Oh well I guess I can just go home now. Dare I try to delete something else? Who knows what it's going to do next...

So, what went wrong?

When the page first renders, there are 4 rows with 4 link buttons. We never specified an ID for those link buttons when we created them. That means they get automatically generated IDs. The rows and cells do, too, but only the link button actually requires an ID to be rendered, because it must cause a postback via javascript (since links don't naturally do postbacks). If you view the html source, you will see the link button's href looks like "javascript:__doPostBack('ctl01', '')". In this case, ctl01 is the LinkButton ID.

So let's say the link buttons have IDs ctl01, ctl02, ctl03, and ctl04 (the actual IDs are different because of the controls between them, but you get the idea). We click on ctl02 to delete "InfinitiesLoop.jpg". When the postback is processed, first we recreate the table with all 4 rows again. The link buttons will still have the same IDs they did last time. ctl02 raises its command event, we delete the file, and then remove the row from the table. But the link buttons have already determined what their IDs are going to be -- the rendered table will now have link buttons with these IDs: ctl01, ctl03, ctl04. Hmmm. ctl03 is the link button the represents TheFamily.jpg. It will now appear 2nd in the table.

Now we click to delete TheFamily.jpg (ctl03). First the table gets recreated -- only, this time there is no InfinitiesLoop.jpg in the Upload directory. So we only create 3 link buttons this time -- ctl01, ctl02, ctl03. At this stage, ctl03 is the link button that corresponds to TODOList.txt. See the problem yet?

ASP.NET knows who raised the postback event via its ID. But we've pulled the rug out from under it -- the controls on this request have different IDs than they did on the previous request. There happens to be a control with the ID that was posted, but it isn't the one we intended it to be. ASP.NET finds ctrl03, and we delete TODOList.txt. Oops.

This problem can manifest itself in many different ways. If we had textboxes in this table, you might find that they lose their values after certain postbacks, or that their values seem to shift in position.

What to do about it

It's important that the structure of the control tree be the same after a postback as it was before the postback. We actually were not really violating that rule in this case -- we rendered out 3 rows when InfinitiesLoop.jpg was deleted, and we created 3 rows after the postback. But we cheated -- we created 4 rows and removed one, and then on the next request we just created 3 right off the bat. One way we could solve this is by giving each LinkButton a specific ID that will never change. In this case the simplest way would be to make it's ID equal to the file name. There's no way you can confuse one link button with another then. But it's far from ideal to have to stuff the file name into the control ID. The name might be really long. And if we were accessing multiple directories, we could have duplicate file names, so now we'd have to include path information, too. There may be other controls in each row, such as checkboxes that show the file's attributes like whether it is marked as ReadOnly. We'd have to apply this trick to those controls, too. Yuck. Yuck. Yuck.

Really what you want is to be able to "reset" the ID scheme of all the controls in the table. When we remove a row, all the controls after it should 'shift up' with their IDs, so that they will render with the same ID they will have on the next postback. So, what if when we deleted a row, we completely rebuilt the table from the start? Just clear out all the rows and then rebuild it like we originally did...

protected override void OnLoad(EventArgs e) {
    BuildTable();
    base.OnLoad(e);
}
 
private void cmdDelete_Command(object sender, CommandEventArgs e) {
    // command argument contains the file name to delete
    string fileName = Path.GetFileName((string)e.CommandArgument);
    string path = Path.Combine(Server.MapPath("~/uploaded"), fileName);
 
    File.Delete(path);
    this.lblStatus.Text = "Deleted " + fileName;
 
    // removed a row, throw away the table and rebuild it
    this.tblFiles.Rows.Clear();
    BuildTable();
}
 
private void BuildTable() {
    string[] files = Directory.GetFiles(Server.MapPath("~/uploaded"));
 
    foreach (string path in files) {
        // strips directory from path
        string fileName = Path.GetFileName(path);
 
        HtmlTableRow row = new HtmlTableRow();
        // file name cell
        HtmlTableCell cell = new HtmlTableCell();
        cell.InnerText = fileName;
        row.Cells.Add(cell);
 
        // delete cell
        LinkButton cmdDelete = new LinkButton();
        cmdDelete.Text = "delete";
        cmdDelete.CommandArgument = fileName;
        cmdDelete.Command += new CommandEventHandler(cmdDelete_Command);
        cell = new HtmlTableCell();
        cell.Controls.Add(cmdDelete);
        row.Cells.Add(cell);
 
        // add row to table
        this.tblFiles.Rows.Add(row);
    }
}
 

"A" for effort (affort?), but there's still a problem. Now when we click to delete TheFamily.jpg after deleting InfinitiesLoop.jpg, this is what we get.

Delete TheFamily.jpg again

Nothing happens except the postback. If you examine the control IDs this time, you will see that instead of resetting the IDs by clearing out the table and rebuilding it, the rows simply continued the ID sequence. That's because Controls.Clear() does not reset the automatic ID counter unless the control implements INamingContainer. HtmlTable does not implement INamingContainer.


Aside: What is INamingContainer?

INamingContainer is a marker interface, meaning it has no methods to implement. A control "implements" this interface to let the framework know that it plans on giving it's child controls really specific IDs. It's important to the framework, because if a two instances of the same control are on the same page, and the control gives its child controls some specific ID, there'd end up being multiple controls with the same ID on the page, which is going to cause problems. So when a Control is a naming container, the UniqueID for all controls within it will have the parent's ID as a prefix. This scopes it to that control. So while a child control might have ID "foo", its UniqueID will be "parentID$foo" (where parentID = the ID of the parent). Now even if this control exists twice on the page, everyone will still have a UniqueID.

INamingContainer also has the property that any controls within it that do not have a specific ID will have its ID automatically determined based on a counter that is scoped to it. So if there were two naming containers, foo and bar, they might have child controls with UniqueIDs foo$ctl01 and bar$ctl01. Each naming container gets its own little counter.

Note that the ID "foo$ctl01" does not necessarily imply that ctl01 is a direct child control of foo! All it means is that foo is ctl01's naming container (control.NamingContainer). It's parent might be another control which is not a naming container.


So we can solve this problem once and for all by using a custom HtmlTable control that implements INamingContainer. I won't bother showing that... because there's an even better solution.

Remember this scenario is that you don't know "how many" controls there should be at design time. We do, however, know "what" the controls are. You can do it dynamically, if you manage the control tree correctly. But when you know what the controls will be ahead of time, Templating can step in and take care of all of this for you automatically!

<asp:Label ID="lblStatus" runat="server" EnableViewState="false" />
 
<asp:Repeater ID="rptFiles" runat="server" EnableViewState="false">
    <HeaderTemplate>
        <table cellpadding="4" cellspacing="4" border="1">
    </HeaderTemplate>
    <ItemTemplate>
        <tr>
            <td><%# Path.GetFileName((string)Container.DataItem) %></td>
            <td>
                <asp:LinkButton runat="server" Text="delete"
                    CommandArgument="<%# Path.GetFileName((string)Container.DataItem) %>" />
            </td>
        </tr>
    </ItemTemplate>
    <FooterTemplate>
        </table>
    </FooterTemplate>
</asp:Repeater>

Using a repeater, we declare what each row will look like. This is what I mean by "what" vs "how many". We know what, so we can express "what" via the repeater ItemTemplate. The "how many" will come from databinding the repeater, which will repeat the template once for each item we bind to it.

protected override void OnLoad(EventArgs e) {
    this.rptFiles.ItemCommand += new RepeaterCommandEventHandler(rptFiles_ItemCommand);
    BindRepeater();
    base.OnLoad(e);
}
 
private void rptFiles_ItemCommand(object source, RepeaterCommandEventArgs e) {
    // command argument contains the file name to delete
    string fileName = Path.GetFileName((string)e.CommandArgument);
    string path = Path.Combine(Server.MapPath("~/uploaded"), fileName);
 
    File.Delete(path);
    this.lblStatus.Text = "Deleted " + fileName;
 
    // removed a row, rebind the repeater
    BindRepeater();
}
 
private void BindRepeater() {
    string[] files = Directory.GetFiles(Server