您如何解决LostFocus/LostKeyboardFocus问题? [英] How do you solve this LostFocus/LostKeyboardFocus issue?

查看:97
本文介绍了您如何解决LostFocus/LostKeyboardFocus问题?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

好吧,我有一个具有IsEditing属性的控件,出于参数的考虑,该控件具有默认模板,该模板通常是文本块,但是当IsEditing为true时,它将在文本框中交换以进行就地编辑.现在,当控件失去焦点时,如果仍在编辑,它应该退出编辑模式并换回TextBlock模板.很简单吧?

考虑在Windows资源管理器或桌面上重命名文件的行为(这是我所知道的...),这就是我们想要的行为.

问题是您不能使用LostFocus事件,因为当您切换到另一个窗口(或作为FocusManager的元素)时,由于控件仍然具有逻辑焦点,因此不会触发LostFocus.

如果您改用LostKeyboardFocus,虽然确实解决了其他FocusManager"问题,那么现在您有了一个新问题:编辑时,您右键单击文本框以显示上下文菜单,因为上下文菜单现在具有键盘焦点,您的控件将失去键盘焦点,退出编辑模式并关闭上下文菜单,使用户感到困惑!

现在,我尝试设置一个标志以在菜单打开之前忽略LostKeyboardFocus,然后在LostKeyboardFocus事件中使用该错误确定是否将其踢出编辑模式,但是如果菜单是打开的,我单击在应用程序中的其他位置,由于控件本身不再具有键盘焦点(菜单具有菜单焦点),因此控件再也不会收到另一个LostKeyboardFocus事件,因此它将保持在编辑模式. (我可能不得不在菜单关闭时添加检查以查看焦点所在,如果不是控件,则将其手动踢出EditMode.这似乎很有希望.)

所以...有人知道我如何成功编写此行为?

标记

解决方案

好吧...就像在Programmer-fun中一样,这很有趣".弄清楚了keester的确很痛苦,但脸上却挂满了漂亮的笑容. (考虑到我自己这么努力拍拍,现在该为我的肩膀买些IcyHot了!:P)

无论如何,这是一个多步骤的事情,但是一旦您弄清了一切,它就会非常简单.简短的版本是您需要同时使用 LostFocus LostKeyboardFocus,而不是一个.

LostFocus很简单.每当您收到该事件时,请将IsEditing设置为false.完成并完成.

上下文菜单和键盘焦点丢失

LostKeyboardFocus有点棘手,因为控件的上下文菜单可以在控件本身上触发它(即,当控件的上下文菜单打开时,控件仍然具有焦点,但是失去了键盘焦点,因此,LostKeyboardFocus射击.)

要处理此行为,请覆盖ContextMenuOpening(或处理事件)并设置一个类级别标志,指示菜单正在打开. (我使用bool _ContextMenuIsOpening.)然后在LostKeyboardFocus覆盖(或事件)中,检查该标志,如果已设置该标志,则只需清除该标志即可,而无需执行其他任何操作.但是,如果未设置,则意味着上下文菜单打开以外的其他原因导致控件失去键盘焦点,因此在这种情况下,您确实希望将IsEditing设置为false.

已经打开的上下文菜单

现在有一个奇怪的行为,如果打开了控件的上下文菜单,因此控件如上所述已经失去了键盘焦点,如果您单击应用程序中的其他位置,则在新控件获得焦点之前,您的控件将获得键盘焦点位于第一位,但只有一秒钟,然后立即将其交给新控件.

这实际上对我们有利,因为这意味着我们还将获得另一个LostKeyboardFocus事件,但是这次_ContextMenuOpening标志将被设置为false,并且如上所述,我们的LostKeyboardFocus处理程序将随后设置IsEditing为false,这正是我们想要的.我喜欢偶然!

现在将焦点简单地移到了您单击的控件上,而没有先将焦点重新设置为拥有上下文菜单的控件,然后我们必须做一些事情,例如钩住ContextMenuClosing事件并检查将要使用的控件接下来要获得焦点,那么如果即将成为焦点的控件不是生成上下文菜单的控件,则仅将IsEditing设置为false,因此我们基本上在此处避开了一个项目符号.

注意:默认上下文菜单

现在还需要注意的是,如果您正在使用文本框之类的东西,并且没有在其上显式设置自己的上下文菜单,那么您不要获得ContextMenuOpening事件,该事件让我吃了一惊.但是,只需使用与默认上下文菜单相同的标准命令(例如,剪切,复制,粘贴等)创建一个新的上下文菜单,然后将其分配给文本框,即可轻松解决此问题.看起来完全一样,但是现在您需要设置标志.

但是,即使您遇到问题,好像您正在创建第三方可重用控件,并且该控件的用户希望拥有自己的上下文菜单,您也可能会不小心将其设置为更高的优先级,并且会压倒他们的!

解决方法是因为文本框实际上是控件的IsEditing模板中的一项,所以我只是在外部控件上添加了一个名为IsEditingContextMenu的新DP,然后通过内部TextBox样式,然后添加该样式的DataTrigger来检查外部控件上的IsEditingContextMenu值,如果该值为空,则设置上面刚刚创建的默认菜单,该菜单存储在资源中.

这是文本框的内部样式(名为"Root"的元素表示用户实际在XAML中插入的外部控件)...

<Style x:Key="InlineTextbox" TargetType="TextBox">

    <Setter Property="OverridesDefaultStyle" Value="True"/>
    <Setter Property="FocusVisualStyle"      Value="{x:Null}" />
    <Setter Property="ContextMenu"           Value="{Binding IsEditingContextMenu, ElementName=Root}" />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBoxBase}">

                <Border Background="White" BorderBrush="LightGray" BorderThickness="1" CornerRadius="1">
                    <ScrollViewer x:Name="PART_ContentHost" />
                </Border>

            </ControlTemplate>
        </Setter.Value>
    </Setter>

    <Style.Triggers>
        <DataTrigger Binding="{Binding IsEditingContextMenu, RelativeSource={RelativeSource AncestorType=local:EditableTextBlock}}" Value="{x:Null}">
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Command="ApplicationCommands.Cut" />
                        <MenuItem Command="ApplicationCommands.Copy" />
                        <MenuItem Command="ApplicationCommands.Paste" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
        </DataTrigger>
    </Style.Triggers>

</Style>

请注意,您必须在样式中设置初始上下文菜单绑定,而不是直接在文本框上设置绑定,否则样式的DataTrigger将被直接设置的值所取代,从而使触发器无用,并且如果情况发生,您将回到第二个平方该人在上下文菜单中使用"null". (如果您想取消显示菜单,则无论如何都不会使用'null'.将其设置为空菜单,因为null表示'使用默认值')

因此,当IsEditing为false时,用户现在可以使用常规的ContextMenu属性...当IsEditing为true时,他们可以使用IsEditingContextMenu;如果未指定IsEditingContextMenu,则内部我们定义的默认值用于文本框.由于文本框的上下文菜单实际上永远不会为空,因此它的ContextMenuOpening始终会触发,因此支持这种行为的逻辑起作用.

就像我说的那样……真正的痛苦可以弄清这一切,但是如果我在这里没有很酷的成就感,那就该死.

我希望这对这里的其他人有帮助.随时在这里回复或向我提问.

标记

Ok, I have a control that has an IsEditing property which for argument's sake has a default template that is normally a text block, but when IsEditing is true, it swaps in a textbox for in-place editing. Now when the control loses focus, if it's still editing, it's supposed to drop out of editing mode and swap back in the TextBlock template. Pretty straight forward, right?

Think of the behavior of renaming a file in Windows Explorer or on your desktop (which is the same thing I know...) That's the behavior we want.

The issue is you can't use the LostFocus event because when you switch to another window (or element that is a FocusManager) LostFocus doesn't fire since the control still has logical focus, so that won't work.

If you instead use LostKeyboardFocus, while that does solve the 'other FocusManager' issue, now you have a new one: when you're editing and you right-click on the textbox to show the context menu, because the context menu now has keyboard focus, your control loses keyboard focus, drops out of edit mode and closes the context menu, confusing the user!

Now I've tried setting a flag to ignore the LostKeyboardFocus just before the menu opens, then using that fiag in the LostKeyboardFocus event to determine to kick it out of editing mode or not, but if the menu is open and I click elsewhere in the app, since the control itself didn't have keyboard focus anymore (the menu had it) the control never gets another LostKeyboardFocus event so it remains in edit mode. (I may have to add a check when the menu closes to see what has focus then manually kick it out of EditMode if it's not the control. That seems promising.)

So... anyone have any idea how I can successfully code this behavior?

Mark

解决方案

Ok... this was "fun" as in Programmer-fun. A real pain in the keester to figure out, but with a nice huge smile on my face that I did. (Time to get some IcyHot for my shoulder considering I'm patting it myself so hard! :P )

Anyway it's a multi-step thing but is surprisingly simple once you figure out everything. The short version is you need to use both LostFocus and LostKeyboardFocus, not one or the other.

LostFocus is easy. Whenever you receive that event, set IsEditing to false. Done and done.

Context Menus and Lost Keyboard Focus

LostKeyboardFocus is a little more tricky since the context menu for your control can fire that on the control itself (i.e. when the context menu for your control opens, the control still has focus but it loses keyboard focus and thus, LostKeyboardFocus fires.)

To handle this behavior, you override ContextMenuOpening (or handle the event) and set a class-level flag indicating the menu is opening. (I use bool _ContextMenuIsOpening.) Then in the LostKeyboardFocus override (or event), you check that flag and if it's set, you simply clear it and do nothing else. If it's not set however, that means something besides the context menu opening is causing the control to lose keyboard focus, so in that case you do want to set IsEditing to false.

Already-Open Context Menus

Now there's an odd behavior that if the context menu for a control is open, and thus the control has already lost keyboard focus as described above, if you click elsewhere in the application, before the new control gets focus, your control gets keyboard focus first, but only for a split second, then it instantly yields it to the new control.

This actually works to our advantage here as this means we'll also get another LostKeyboardFocus event but this time the _ContextMenuOpening flag will be set to false, and just like described above, our LostKeyboardFocus handler will then set IsEditing to false, which is exactly what we want. I love serendipity!

Now had the focus simply shifted away to the control you clicked on without first setting the focus back to the control owning the context menu, then we'd have to do something like hooking the ContextMenuClosing event and checking what control will be getting focus next, then we'd only set IsEditing to false if the soon-to-be-focused control wasn't the one that spawned the context menu, so we basically dodged a bullet there.

Caveat: Default Context Menus

Now there's also the caveat that if you are using something like a textbox and haven't explicitly set your own context menu on it, then you don't get the ContextMenuOpening event, which surprised me. That's easily fixed however, by simply creating a new context menu with the same standard commands as the default context menu (e.g. cut, copy, paste, etc.) and assigning it to the textbox. It looks exactly the same, but now you get the event you need to set the flag.

However, even there you have an issue as if you're creating a third-party-reusable control and the user of that control wants to have their own context menu, you may accidentally set yours to a higher precedence and you'll override theirs!

The way around that was since the textbox is actually an item in the IsEditing template for my control, I simply added a new DP on the outer control called IsEditingContextMenu which I then bind to the textbox via an internal TextBox style, then I added a DataTrigger in that style that checks the value of IsEditingContextMenu on the outer control and if it's null, I set the default menu I just created above, which is stored in a resource.

Here's the internal style for the textbox (The element named 'Root' represents the outer control that the user actually inserts in their XAML)...

<Style x:Key="InlineTextbox" TargetType="TextBox">

    <Setter Property="OverridesDefaultStyle" Value="True"/>
    <Setter Property="FocusVisualStyle"      Value="{x:Null}" />
    <Setter Property="ContextMenu"           Value="{Binding IsEditingContextMenu, ElementName=Root}" />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBoxBase}">

                <Border Background="White" BorderBrush="LightGray" BorderThickness="1" CornerRadius="1">
                    <ScrollViewer x:Name="PART_ContentHost" />
                </Border>

            </ControlTemplate>
        </Setter.Value>
    </Setter>

    <Style.Triggers>
        <DataTrigger Binding="{Binding IsEditingContextMenu, RelativeSource={RelativeSource AncestorType=local:EditableTextBlock}}" Value="{x:Null}">
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Command="ApplicationCommands.Cut" />
                        <MenuItem Command="ApplicationCommands.Copy" />
                        <MenuItem Command="ApplicationCommands.Paste" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
        </DataTrigger>
    </Style.Triggers>

</Style>

Note that you have to set the initial context menu binding in the style, not directly on the textbox or else the style's DataTrigger gets superseded by the directly-set value rendering the trigger useless and you're right back to square one if the person uses 'null' for the context menu. (If you WANT to suppress the menu, you wouldn't use 'null' anyway. You'd set it to an empty menu as null means 'Use the default')

So now the user can use the regular ContextMenu property when IsEditing is false... they can use the IsEditingContextMenu when IsEditing is true, and if they didn't specify an IsEditingContextMenu, the internal default that we defined is used for the textbox. Since the textbox's context menu can never actually be null, its ContextMenuOpening always fires, and therefore the logic to support this behavior works.

Like I said... REAL pain in the can figuring this all out, but damn if I don't have a really cool feeling of accomplishment here.

I hope this helps others here with the same issue. Feel free to reply here or PM me with questions.

Mark

这篇关于您如何解决LostFocus/LostKeyboardFocus问题?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆