views:

69

answers:

3

I'm writing a simple extension method to perform an action on a control and all of its children, and I'm wondering if I have to worry about running into the same control twice.

Safe:

public static void Traverse(this Control control, Action<Control> action)
{
    Traverse(control, action, new HashSet<control>());
}

private static void Traverse(this Control control, Action<Control> action, HashSet<Control> handled)
{
    handled.Add(control);
    foreach (Control child in control.Controls)
        if (!handled.Contains(child))
            Traverse(child, action, handled);
    action.Invoke(control);
}

Possibly Unsafe:

public static void Traverse(this Control control, Action<Control> action)
{
    foreach (Control child in control.Controls)
        Traverse(child, action, handled);
    action.Invoke(control);
}

Is the hash set necessary to keep this code safe? It needs to invoke the action on every control only once, and it can't enter an infinite loop. Is the structure of parent-child controls such that I don't need to worry about this?

Usage:

this.Traverse(o => o.SuspendLayout());

// Do lots of UI changes

this.Traverse(o => o.ResumeLayout());

The (possibly) comprehensive way to do this:

public static class ControlExtensions
{
    public static void Traverse(this Control control, Action<Control> action)
    {
        Traverse(control, action, TraversalMethod.DepthFirst);
    }

    public static void Traverse(this Control control, Action<Control> action, TraversalMethod method)
    {
        switch (method)
        {
            case TraversalMethod.DepthFirst:
                TraverseDepth(control, action);
                break;
            case TraversalMethod.BreadthFirst:
                TraverseBreadth(control, action);
                break;
            case TraversalMethod.ReversedDepthFirst:
                TraverseDepthReversed(control, action);
                break;
            case TraversalMethod.ReversedBreadthFirst:
                TraverseBreadthReversed(control, action);
                break;
        }
    }

    private static void TraverseDepth(Control control, Action<Control> action)
    {
        Stack<Control> controls = new Stack<Control>();
        Queue<Control> queue = new Queue<Control>();

        controls.Push(control);
        while (controls.Count != 0)
        {
            control = controls.Pop();
            foreach (Control child in control.Controls)
                controls.Push(child);
            queue.Enqueue(control);
        }
        while (queue.Count != 0)
            action.Invoke(queue.Dequeue());
    }

    private static void TraverseBreadth(Control control, Action<Control> action)
    {
        Queue<Control> controls = new Queue<Control>();
        Queue<Control> queue = new Queue<Control>();

        controls.Enqueue(control);
        while (controls.Count != 0)
        {
            control = controls.Dequeue();
            foreach (Control child in control.Controls)
                controls.Enqueue(child);
            queue.Enqueue(control);
        }
        while (queue.Count != 0)
            action.Invoke(queue.Dequeue());
    }

    private static void TraverseDepthReversed(Control control, Action<Control> action)
    {
        Stack<Control> controls = new Stack<Control>();
        Stack<Control> stack = new Stack<Control>();

        controls.Push(control);
        while (controls.Count != 0)
        {
            control = controls.Pop();
            foreach (Control child in control.Controls)
                controls.Push(child);
            stack.Push(control);
        }
        while (stack.Count != 0)
            action.Invoke(stack.Pop());
    }

    private static void TraverseBreadthReversed(Control control, Action<Control> action)
    {
        Queue<Control> controls = new Queue<Control>();
        Stack<Control> stack = new Stack<Control>();

        controls.Enqueue(control);
        while (controls.Count != 0)
        {
            control = controls.Dequeue();
            foreach (Control child in control.Controls)
                controls.Enqueue(child);
            stack.Push(control);
        }
        while (stack.Count != 0)
            action.Invoke(stack.Pop());
    }
}
+3  A: 

Each child has one parent, so there's no need to worry.

Steven Sudit
ahh, but what if the starting control has no parent? I have seen this happen, btw
Muad'Dib
We're looking at the children of a given control, recursively. This means we never look up at the parent, instead using the stack to keep track of that lineage. As a result, it'll work just fine when first control is parentless.
Steven Sudit
+2  A: 

Controls really can only have a single parent. There isn't really a reason to track "handled", as you'll only execute your method on the control a single time.

Now, if you're using a framework that allows controls to have multiple parents (I'm not aware of any .NET frameworks that allow this), then this might be required. If, however, you're using Windows Forms (which is what this appears to be) or WPF, you can just simplify this to:

private static void Traverse(this Control control, Action<Control> action)
{
    foreach (Control child in control.Controls)
        Traverse(child, action);
    action(control);
}
Reed Copsey
Or if the action being performed during traversal *changes* parenthood...
Steven Sudit
In that case, you're going to have other issues. It's tough to imagine a safe method that's going to be reparenting controls in a method intended to "perform an action on a control and all of its children"
Reed Copsey
@Steven: Also, if you're changing the parenthood, you'll have problems in the foreach, even with "handled" in place...
Reed Copsey
Good point. Changing parents would effectively change the contents of each collection, and that's a foreach no-no. I guess the only choice would be the usual work-around: iterate once to build a to-do list, then go through and implement the list.
Steven Sudit
A: 

You need to use recursion

public sub DoStuffToControlAndChildren(TargetControl as Control)

    'Insert code to do stuff to TargetControl here

     if TargetControl.Controls.count = 0 then
         return
     end if 

     For each ChildControl in TargetControl.Controls
        DoStuffToControlAndChildren(ChildControl)
     next

end sub
Achilles
I opted away from recursion by keeping stacks/queues.
Daniel Rasmussen
@Daniel: How deep is a tree really going to be? If the answer is "not very", then recursion will be just fine.
Steven Sudit
The answer *is* "not very," but I enjoy the elegance (IMO) of the stacks and queues, and this was both a way to traverse controls as well as an experiment in traversal in general, so I'd like to keep the algorithm as broad as possible. Is there any reason *not* to use stacks and queues? Is the overhead not worth it when you're only traversing a hundred controls nested several deep?
Daniel Rasmussen
@Daniel: To be frank, I think that it would work just fine either way. There are even some general arguments against using recursion in .NET, based on delaying the collection of locals. In this case, I suspect that recursion will perform at least as well as explicitly allocating a stack, and I feel that it's more elegant. Clearly, you disagree with the latter. :-)
Steven Sudit