For posterity, here is the function I ended up writing. Works very well (tested in a real project):
public static Control PreviousControl(this Control control)
{
ControlCollection siblings = control.Parent.Controls;
for (int i = siblings.IndexOf(control) - 1; i >= 0; i--)
{
if (siblings[i].GetType() != typeof(LiteralControl) && siblings[i].GetType().BaseType != typeof(LiteralControl))
{
return siblings[i];
}
}
return null;
}
To be used like this:
Control panel = textBox.PreviousControl();
and for next control:
public static Control NextControl(this Control control)
{
ControlCollection siblings = control.Parent.Controls;
for (int i = siblings.IndexOf(control) + 1; i < siblings.Count; i++)
{
if (siblings[i].GetType() != typeof(LiteralControl) && siblings[i].GetType().BaseType != typeof(LiteralControl))
{
return siblings[i];
}
}
return null;
}
The advantage of this solution over that of Atzoya is that, first, you don't need the original control to have an ID since I do the search based on instance. Second, you have to know that ASP.net generates several Literal controls in order to render your static HTML in between your "real" controls. That's why I skip them, or you will keep matching junk. Of course the downside of this is you can't find a control if it's a Literal. This limitation was not a problem in my use.