By using this site, you agree to our updated Privacy Policy and our Terms of Use. Manage your Cookies Settings.
434,806 Members | 1,506 Online
Bytes IT Community
+ Ask a Question
Need help? Post your question and get tips & solutions from a community of 434,806 IT Pros & Developers. It's quick & easy.

Problem with GetEnumeration in Foreach loop

tlhintoq
Expert 2.5K+
P: 3,525
Situation:
A Windows Form with a single User Control containing 4 custom controls. This lets me drop the User Control on its own form to make it a dialog, or make it part of a larger configuration dialog with several such User Controls.
Each custom control has a property for COM port name (serial port).
Each custom control has a method for writing the settings.
The user control has a GetEnumeration structure so it can be easily referenced with a foreach (myCustomControl Bob in myUserControl) type reference.

Errant behavior:
If I walk through using the foreach construct everything executes correctly THEN all the custom controls are inexplicably rendered invisible. They still exist because I can step through the save function and they all perform their methods. They just become hidden on the Windows form.
Yet - If I walk through the same steps with a for (int a = [...]) type construct doing exactly the same things everything executes and the custom controls are NOT hidden.

Its not like the loop is doing a thousand tasks and the process is hard to follow:
1) Set port name
2) Write settings

It really shouldn't matter *how* the set of controls is looped through. Both the 'for' and 'foreach' *should* being doing exactly the same thing: Loop through the four controls, set the port name property and execute the WritePrefs() method. That's it. Yet for some reason after the 'foreach' completes all the custom controls go invisible.

Something in the bowels of the foreach seems to be reacting to the custom controls .Show() and .Hide()

Does anyone have a clue or has anyone seen something like this before?
Obviously I have already worked around the weird behavior by using a 'for' instead of 'foreach'. But I'd like to understand the actual problem so I can use the more elegant 'foreach' in the future.

The foreach that hides the custom controls
Expand|Select|Wrap|Line Numbers
  1.             foreach (Saint.Controls.Devices.DeviceCtrl pebbles in Controller1)
  2.             {
  3.                 pebbles.myObject.PortName = myCOMport.PortName;
  4.                 pebbles.myObject.WritePrefs();
  5.             }
  6.  
The for that does the same thing without hiding the custom controls.
Expand|Select|Wrap|Line Numbers
  1.             for (int a = 0; a < Controller1.Controls.Count; a++)
  2.             {
  3.                 ((Saint.Controls.Devices.DeviceCtrl) Controller1.Controls[a]).myObject.PortName = myCOMport.PortName;
  4.                 ((Saint.Controls.Devices.DeviceCtrl) Controller1.Controls[a]).myObject.WritePrefs();
  5.             }
  6.  
The class with the GetEnumeration
Expand|Select|Wrap|Line Numbers
  1.     public partial class Controller1: UserControl, IEnumerable, IEnumerator
  2.     {
  3.         ArrayList AllControls = new ArrayList();
  4.  
  5.         #region GetEnumeration
  6.         int Position = -1;
  7.  
  8.         public IEnumerator GetEnumerator()
  9.         {
  10.             return (IEnumerator)this;
  11.         }
  12.         public bool MoveNext()
  13.         {
  14.             if (Position < AllControls.Count - 1)
  15.             {
  16.                 ++Position;
  17.                 return true;
  18.             }
  19.             Reset();
  20.             return false;
  21.         }
  22.         public void Reset()
  23.         {
  24.             Position = -1;
  25.         }
  26.         public object Current
  27.         {
  28.             get
  29.             {
  30.                 return AllControls[Position];
  31.             }
  32.         }
  33.         #endregion GetEnumeration
  34.  
  35.  
  36.         #region Construction
  37.         public Controller1()
  38.         {
  39.             InitializeComponent();
  40.             AllControls.Add(tcntrlWiper);
  41.             AllControls.Add(tcntlWasher);
  42.             AllControls.Add(tcntrlBlower);
  43.             AllControls.Add(tcntrlDefroster);
  44.         }
  45.         #endregion Construction
  46.     }
  47.  
Feb 5 '09 #1
Share this Question
Share on Google+
9 Replies


vekipeki
Expert 100+
P: 229
When controls are hidden but still alive, it means they have been removed from the Control.Controls collection. But I don't see what could do that from your code (when comparing 'for' with 'foreach').

The strangest thing is that you don't even have to access your controls to have them removed from Controls collection, try this:

Expand|Select|Wrap|Line Numbers
  1. foreach (Control c in Controller1)
  2. {
  3.     // this will show 4
  4.     Console.WriteLine(Controller1.Controls.Count);
  5.  
  6.     // break immediately after getting the 1st control
  7.     break;
  8. }
  9. // this will write 0
  10. Console.WriteLine(Controller1.Controls.Count);
  11.  
That's pretty weird!

You can use foreach directly on Controller1.Controls collection, instead of implementing IEnumerable, or implement IEnumerable just as a wrapper for Controls.GetEnumerator():

Expand|Select|Wrap|Line Numbers
  1. public IEnumerator GetEnumerator()
  2. {
  3.     return this.Controls.GetEnumerator();
  4. }
  5.  
(this last part doesn't solve the actual problem, but it works this way)
Feb 5 '09 #2

tlhintoq
Expert 2.5K+
P: 3,525
I'm not convinced the controls are being removed - only because if I run the function a second time and step through it still loops four times receiving the Wiper, Washer, Blower, Defroster controls. If they were actually being removed the the controls collection after the first run of the function, wouldn't the second run have no controls to loop through?
Feb 5 '09 #3

Plater
Expert 5K+
P: 7,872
Both you made your foreach loop in the same format:
foreach (Control c in Controller1)

If I were to have used the loop, I would have said
foreach (Control c in Controller1.Controls)

Not sure it matters, but I find it interesting that you don't actually reference your exact collection object?
Feb 5 '09 #4

tlhintoq
Expert 2.5K+
P: 3,525
There was actually a reason for doing this way. My GetEnumeration is referencing only an array of custom controls. Right now that array is exactly the same as the controls collection. But I try to plan ahead and could see a time where the controls collection might include lables, pictureboxes, group boxes etc., yet I would want the foreach to only return the significant custom controls and not each and every little piece of decoration added to the User Control panel. Or maybe I only want to perform a function on *some* custom controls like controllable devices, but not all custom controls such as readout only sensors... So it made sense that I should be able to make a list of the objects that I want to be included in the enumeration and ignore others.

After some experimentation I found some more interesting results...
This works without hiding the controls... just as was suggested... by adding the reference to the .Controls collection.
Expand|Select|Wrap|Line Numbers
  1.             foreach (Saint.Controls.Devices.DeviceCtrl pebbles in Controller1.Controls)
  2.             {
  3.                 Console.WriteLine("TCCC: " + Controller1.Controls.Count);
  4.                 pebbles.myObject.PortName = myCOMport.PortName;
  5.                 pebbles.myObject.WritePrefs();
  6.             }
  7.  
So I figured Okay, I'll adjust the GetEnumeration to work with the controls collection to see what happens. It should be exactly the same since I am referencing the same array. So I comment out all the reference to my array and change lines 14 & 30 to directly reference the .Controls.

Expand|Select|Wrap|Line Numbers
  1.     public partial class Controller1: UserControl, IEnumerable, IEnumerator
  2.     {
  3.         //ArrayList AllControls = new ArrayList();
  4.  
  5.         #region GetEnumeration
  6.         int Position = -1;
  7.  
  8.         public IEnumerator GetEnumerator()
  9.         {
  10.             return (IEnumerator)this;
  11.         }
  12.         public bool MoveNext()
  13.         {
  14.             if (Position < this.Controls.Count - 1)
  15.             {
  16.                 ++Position;
  17.                 return true;
  18.             }
  19.             Reset();
  20.             return false;
  21.         }
  22.         public void Reset()
  23.         {
  24.             Position = -1;
  25.         }
  26.         public object Current
  27.         {
  28.             get
  29.             {
  30.                 return this.Controls[Position];
  31.             }
  32.         }
  33.         #endregion GetEnumeration
  34.  
  35.  
  36.         #region Construction
  37.         public ThrillControllerCntrl()
  38.         {
  39.             InitializeComponent();
  40.             //AllControls.Add(tcntrlWiper);
  41.             //AllControls.Add(tcntlWasher);
  42.             //AllControls.Add(tcntrlBlower);
  43.             //AllControls.Add(tcntrlDefroster);
  44.         }
  45.         #endregion Construction
  46.     }
  47.  
  48.  
Now that should have worked whether my foreach loop referenced the .Controls collection or not, right? - NOPE - It still hides the custom controls upon completion if I don't specify the .Collection in the foreach call construct.
-BUT- This does now remove the control from the collection: The second time through the function the collection count becomes zero.

As that weird or what?

Just to clarify one other thing... I got thinking that if I added a label(or something else) to the panel that maybe it wouldn't be a problem since the "foreach" call does specify an exact custom type. -NOPE- All controls of all types are returned and the code breaks as soon as it tries to cast a label to my custom type. - Which I had expected and why I wanted my GetEnumeration to only return exactly what I specified so I didn't have to filter at every point I used the call.
Expand|Select|Wrap|Line Numbers
  1. Unable to cast object of type 'System.Windows.Forms.Label' to type 'Saint.Controls.Devices.DeviceCtrl'.
  2.  
Seems rather limiting, but I can work around it - for now. I would have expected that my class should be able to specify what it will and won't return via my GetEnumeration method.
Feb 5 '09 #5

vekipeki
Expert 100+
P: 229
That's my point exactly, your controls are removed from the Controls collection, not AllControls collection. Forms' controls are drawn only if they are part of the Controls collection.

Actually, your controls are removed from the Controls collection if you just implement IEnumerator, regardless of what it actually accesses -- check this out:

Expand|Select|Wrap|Line Numbers
  1. #region IEnumerable
  2.  
  3. public IEnumerator GetEnumerator()
  4. {
  5.  return this;
  6. }
  7.  
  8. #endregion
  9.  
  10. #region IEnumerator
  11.  
  12. public object Current
  13. {
  14.     get { return new object(); } // we're not accessing anything
  15. }
  16.  
  17. public bool MoveNext()
  18. {
  19.     return false; // never enter the foreach loop
  20. }
  21.  
  22. public void Reset()
  23. {
  24.     // do nothing
  25. }
  26.  
  27. #endregion
It still does the same thing! Only because you implemented that interface, regardless of the fact that MoveNext returns false, and the foreach loop is never entered!

To make it even stranger, you can completely remove AllControls list to see that it is doesn't make any difference - it's only the IEnumerator!

But, this will work, and will allow you to have a custom collection:

Expand|Select|Wrap|Line Numbers
  1. public IEnumerator GetEnumerator()
  2. {
  3.  return AllControls.GetEnumerator();
  4. }
  5.  
  6. // implementing IEnumerator is no longer needed
  7.  
Feb 6 '09 #6

tlhintoq
Expert 2.5K+
P: 3,525
I am about to jump on a plane to Dallas for an installation. As soon as I get back next week I will play with this. Thank you very much for the code. I will pour over exactly how it works so that I can use it to just like the custom controls. I never like having code that works if I don't actually UNDERSTAND the why behind it. I'm guessing that I just made it more complicated than it needed to be; assuming I had to do more by hand than needed.
Feb 6 '09 #7

tlhintoq
Expert 2.5K+
P: 3,525
That's my point exactly, your controls are removed from the Controls collection, not AllControls collection. Forms' controls are drawn only if they are part of the Controls collection.
I would still like to understand why and where they are being removed *at all*. I totally understand that the form can't display items if they aren't in the item's collection. But why or where is the enumeration process removing them from the collection?
Feb 6 '09 #8

Plater
Expert 5K+
P: 7,872
Perhaps your enumerator screws up the pointers to the collection object's natural pointer.
Like if your objects have internal enumeration pointers, and the collection object has one that points to those pointers....when you make your own enumeration it changes the pointers and the collection pointer now points to something that isn't there, it assumes it must be empty.

Like if you had a linked list with X amount of items, and removed the first item in the list, but didn't update the HEAD pointer first, the HEAD pointer would be looking at garbage and not at a list member anymore, so a traversal would appear empty?

That might not make sense to anyone else, but seems plausable to me.
Feb 6 '09 #9

vekipeki
Expert 100+
P: 229
I just used ildasm to look at the IL for a small app, which called foreach on a control implementing IEnumerator.

And there was the answer: right after foreach ends, there is a check to see if the returned IEnumerator implements IDisposable, and if yes, it calls Dispose() to dispose it. To be sure, I added a breakpoint in the overriden Dispose(bool disposing) method and it was really called right after foreach.

So, in other words, CLR disposes the enumerator after use, but the enumerator is actually your control instance.

I guess this is why they always implement IEnumerator in a private class and instantiate on every call to GetEnumerator - so it can be collected immediately! :)
Feb 8 '09 #10

Post your reply

Sign in to post your reply or Sign up for a free account.