今天写了段VB。Net遍历控件的方法,乍一看没有什么问题,但结果却出人意料,control集合的元素没有遍历完,并且有重复的元素。
Public Sub SetControlLableAndPropertyFromXml(ByVal parent As Control)
For Each subControl As Control In parent.Controls
If subControl.HasChildren Then
SetControlLableAndPropertyFromXml(subControl)
End If
If mShosaiConfig.ShosaiSetting.Items(subControl.Name) Is Nothing Then
Continue For
End If
subControl.Text = mShosaiConfig.ShosaiSetting.Items(subControl.Name).LableText
Dim editCtl = subControl.Parent.Controls.Item(mShosaiConfig.ShosaiSetting.Items(subControl.Name).CompoName)
If Not editCtl Is Nothing Then
If Not mShosaiConfig.ShosaiSetting.Items(subControl.Name).Editable Is Nothing Then
editCtl.Enabled = mShosaiConfig.ShosaiSetting.Items(subControl.Name).Editable
End If
End If
Next
End Sub
我一直以为是foreach改变了读取数据的数序,或者说他可能不是按顺序读取,但用reflecter看了下,Controls是ControlConnection集合,这个集合实现了Ienumerable接口,其本质和用for语句遍历是一样的,他们都是index + 1 的方式,用索引来访问的。所以问题不在foreach语句。
最后发现是这一句editCtl.Enabled = mShosaiConfig.ShosaiSetting.Items(subControl.Name).Editable出了问题。editCtl是重写过的Combox控件,在OnEnabledChanged事件中用了,SendToBack和BringToFront方法。
Protected Overrides Sub OnEnabledChanged(ByVal e As System.EventArgs)
MyBase.OnEnabledChanged(e)
If Me.Enabled Then
Me.m_lblComboText.Visible = False
Else
Me.m_lblComboText.Visible = True
Me.SendToBack()
Me.m_lblComboText.BringToFront()
End If
End Sub
罪魁祸首就是这个SendToBack和BringToFront,这两个函数的使用,可能会导致控件所在的父控件(parent)的Controls集合元素移位,也就是如果我们在for,foreach语句中,对Controls集合的元素调用了这两个方法,由于Controls的元素移动了,再按index +1 的方式来遍历可能有未遍历的元素移动到了Controls集合的前面,造成遍历不完全。同理,已经遍历的元素移动到Controls的后面会造成元素的重复。
SendToBack和BringToFront会改变控件的“Zindex“值来改变控件的显示层次,来看看他的内部实现:
public void SendToBack()
{
if (this.parent != null)
{
this.parent.Controls.SetChildIndex(this, -1);
}
else if (this.IsHandleCreated && this.GetTopLevel())
{
SafeNativeMethods.SetWindowPos(new HandleRef(this.window, this.Handle), NativeMethods.HWND_BOTTOM, 0, 0, 0, 0, 3);
}
}
==》
public virtual void SetChildIndex(Control child, int newIndex)
{
this.SetChildIndexInternal(child, newIndex);
}
==》
internal virtual void SetChildIndexInternal(Control child, int newIndex)
{
if (child == null)
{
throw new ArgumentNullException("child");
}
int childIndex = this.GetChildIndex(child);
if (childIndex != newIndex)
{
if ((newIndex >= this.Count) || (newIndex == -1))
{
newIndex = this.Count - 1;
}
base.MoveElement(child, childIndex, newIndex);
child.UpdateZOrder();
LayoutTransaction.DoLayout(this.owner, child, PropertyNames.ChildIndex);
}
}
关键就是这句base.MoveElement(child, childIndex, newIndex);会造成Controls内元素的重新排列。
那么对要遍历Controls,并且要改变其中Control元素显示层次应该怎么办呢?
法一:声明一个List<Control> lstControls,先遍历controls,把这些元素加到lstControls中,再对lstControls遍历,调用SendToBack方法。这种方法主要是避开对ControlConnection集合的遍历。
法二:已经知道要遍历控件name的场合,可以用parent.Controls.Find(controlName, True)来查找,再调用SendToBack方法。
也许还有其它更好方法,有待研究。