一、问题重现步骤:
1. 新建一个Winforms项目;
2. 在项目中添加一个UserControl,并在里面添加两个TextBox:textBox1和textBox2;
3. 编译该项目;
4. 在Toolbox里找到我们添加的UserControl1,并在Form1上添加一个实例userControl11;
5. 在Form1添加一个TextBox,命名为textBox3;
6. 为textBox3添加如下事件处理器(Event Handler):
private void textBox3_Validating(object sender, CancelEventArgs e)
{
MessageBox.Show("textBox3_Validating", "Info");
}
7. 编译、运行;
8. 鼠标点击textBox3,再点击userControl11上的textBox2。
9. 因textBox3_Validating被触发,会弹出一个消息框。点击确认按钮让消息框消失。此时光标在textBox2中;
10. 用键盘输入“abc”
结果:“abc”出现在textBox1中
二、分析:
MessageBox是一个有模式的对话框。当出现一个MessageBox的时候,该MessageBox有自己的消息队列,同时屏蔽Form1的消息队列。所以一旦MessageBox弹出来的时候,Form1就不能响应键盘、鼠标等消息了。当我们点击确认按钮让MessageBox消失的时候,在MessageBox的内部会重新把焦点(Focus)设回到Form1,这样Form1可以继续响应消息。当把焦点设回到Form1时,Winforms会试着让焦点停在Form1上一个可以设置焦点的控件上,在我们这个场景下找到的是textBox1。由于我们点击了textBox2,Winforms同时有试着把焦点设到textBox2,此时就出现了我们前面看到的问题:焦点看起来是在textBox2(因为光标在textBox2),但实际上在textBox1(输入的字符在textBox1)。
textBox3_Validating是在Form1的消息处理过程中被触发了,如果在该事件处理器中弹出一个有模式的对话框,Form1上的消息处理被屏蔽。当焦点重新回到Form1上的时候,它的消息队列已经混乱了。
让textBox3_Validating更加复杂的是,Validating是一个可以取消的事件。当e.Cancel为true的时候,事件处理完后焦点应该仍停在textBox3上;当e.Cancel为false的时候,焦点会设到下一个控件上。在我们的场景里,是textBox2。也就是说,在textBox3_Validating处理结束后,下一个焦点会在那个控件上还是未知的,取决于e.Cancel的值。可是如果弹出一个对话框,在对话框消失的时候会去设置焦点,这个时候无论设到哪个控件上都有可能是错误的。
三、建议
不要在Validating的处理器中弹出对话框。这样会打乱消息队列,很容易导致错误。
如果想绕过(不是解决)前面提到问题,一个办法是用BeginInvoke来弹出对话框。代码如下:
private void textBox3_Validating(object sender, CancelEventArgs e)
{
textBox3.BeginInvoke(new ShowMessageBoxDelegate(ShowMessageBox));
}
private delegate void ShowMessageBoxDelegate();
private void ShowMessageBox()
{
MessageBox.Show("textBox3_Validating", "Info");
}
值得一提的是,该方法不能解决所有的问题。比如当焦点在textBox3的时候去点击textBox2,textBox2是不能收到WM_LBUTTONUP的消息的。因此还是回到前面的建议,不要在Validating中弹出有模式的对话框。