WPF日期选择器失去焦点会触发七次

3

我这里有一个非常简单的场景,请看布局:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>
    <TextBox Grid.Row="0"></TextBox>
    <DatePicker Grid.Row="1" 
                Name="_datePicker"
                LostFocus="_datePicker_OnLostFocus"></DatePicker>
</Grid>

代码和代码后台:

private void _datePicker_OnLostFocus(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("LostFocuse");
}

所以,我的问题是当我选择一个日期并点击TextBox时,事件LostFocus会触发7(七!)次。一次是当我点击TextBoxDatePicker真正失去焦点的时候,剩下的六次对我来说完全无法解释。

我该如何解决?我只需要这个事件触发一次。或者我可以使用其他事件吗?我尝试了LostKeyBoardFocus,但结果相同。


你是否查看了调用栈以确定它是从哪里触发的? - Krishna
@Krishna 来自 WPF 系统中的某个地方。我的 Window 里什么都没有。 - monstr
你能告诉我们在失去焦点事件上你想要做什么或者为什么选择这个事件吗?如果我们了解你的问题背景,我们可以提供一个替代方案。 - Krishna
@Krishna,我需要任何仅在 DatePicker 失去焦点时触发一次的事件。我选择了 LostFocus 事件,因为我认为它会给我想要的行为。 - monstr
3个回答

6

LostFocus是一个路由事件,路由策略设置为Bubble。通过冒泡,它将一直冒泡到其父级,直到根窗口,直到通过明确设置 e.Handled = true; 处理。

因此,这意味着即使子控件失去焦点,它也会冒泡到您的日期选择器,这就是为什么您会看到多个命中您的方法。

您可以检查属性IsKeyboardFocusWithin,该属性返回焦点是否在您的控件内。由于您不想侦听子对象失去焦点事件,因此您可以在您的处理程序中检查此属性,并仅在日期选择器实际失去焦点时执行您的代码:

private void _datePicker_OnLostFocus(object sender, RoutedEventArgs e)
{
    DatePicker picker = sender as DatePicker;
    if (!picker.IsKeyboardFocusWithin)
    {
        System.Diagnostics.Debug.WriteLine("LostFocuse");
    }
}

它有效,谢谢。但如果这个事件是冒泡的,你认为为什么Sajeetharan建议的解决方案不能正常工作呢?设置e.Handled = true;应该停止冒泡,不是吗?但实际上,事件继续冒泡并一遍又一遍地上升。 - monstr
1
这是因为不止一个子控件引发了此事件。在过程中,多个子控件会引发事件,例如CalendarButton等。您可以检查e.OriginalSource以查看哪个子控件引发了事件。无论如何,Sajetharan的解决方案都不正确。当它已经到达datePicker父控件时,他设置了e.Handled = true,这对事件引发没有任何影响。事件需要在子控件上标记为已处理。 - Rohit Vats
嗯...我不太明白。我认为如果我们有冒泡事件并在事件处理程序中设置e.Handled = true;,这必须是停止冒泡的原因。正如你所说,处理程序会在所有子控件上调用,e.Handled = true;将在第一次事件上升时设置,并且它必须会停止冒泡。我有点困惑。 - monstr
1
@monstr - “我认为如果我们有冒泡事件并在事件处理程序中设置e.Handled = true;,这必须是停止冒泡的原因。” - 这是正确的,但您需要在子控件上处理它,但根据Sajeetharan代码,他将其设置为在附加到父控件的处理程序上处理,这意味着它已经冒泡到父级。要阻止其冒泡到父级,您需要在子控件上附加事件并在那里标记它为已处理。 - Rohit Vats
明白了,但它看起来非常奇怪和出乎意料。当“任何”子控件上升事件时,事件处理程序会被调用。但是只有直接附加到父控件的事件处理程序可以设置 e.Handled = true 以产生效果。所以,如果我无法访问子控件,我无法停止冒泡过程,直到它完成在事件处理程序所连接的控件上的过程。很好有停止冒泡的可能性,就像这样 if (e.OriginalSource.GetType() == typeof(SomeChildControl) {e.Handled = true;} - 并且冒泡在 SomeChildControl 类型的控件上停止。 - monstr
1
你可以通过遍历可视树并将处理程序附加到每个控件来从代码后台附加处理程序到子控件。但是,我认为冒泡事件没有问题,因为你始终可以从原始源过滤出实际引发事件的控件。 - Rohit Vats

1
你可以添加一个布尔值来检查第一次,然后将 e.Handled 设置为 true。
bool isFired = false;

private void _datePicker_OnLostFocus(object sender, RoutedEventArgs e)
{
    if (!isFired)
        {
            isFired = true;
        }
        e.Handled = true;

}

它能工作,但这种方法无法抑制事件触发。实际上,事件也会触发7次。还有其他方法吗? - monstr
@monstr 它不会触发7次,可能是你代码的某个部分导致了这个问题。 - Sajeetharan
我发布了所有的代码,我专门为测试制作了一个空白窗口。我尝试在codebehind中使用你的代码。处理程序被调用了六次(不是七次,你是对的 :))。在if(!isFired)条件内的代码将被调用一次,但我强制使用了额外的标志isFired。正如我之前所说,我不喜欢这种方法。 - monstr

1

您所描述的是UIElement.LostFocus事件的.NET正常行为。来自MSDN链接页面的内容如下:

当该元素失去逻辑焦点时发生。

请注意它说的是逻辑焦点... WPF有两种焦点:逻辑和键盘。再次引用链接页面的话:

如果使用方法调用故意强制移开焦点但以前的键盘焦点存在于不同的作用域中,则逻辑焦点与键盘焦点不同。在这种情况下,键盘焦点仍然停留在原处,而调用Focus方法的元素仍然获得逻辑焦点。

最后,为什么会多次触发该事件?因为甚至在DatePicker内的子元素被点击时也可以将其逻辑焦点移走,随着焦点穿过各种内部控件,它可以在短时间内多次返回到DatePicker。再次引用链接页面的话:

由于此事件使用冒泡路由,失去焦点的元素可能是子元素,而不是实际附加事件处理程序的元素。

感谢您的详细解释,但是作为结果,我必须使用哪个DatePicker事件才能在失去焦点时只触发一次? - monstr
没有这样的事件。 - Sheridan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接