12

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

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • 2
    I think it's little too big to read everything. It will be nice if you put little summary, bullet points and some code example if you can put. Only if these thing will look interesting then people might read in detail to answer you. – Akash Kava May 01 '11 at 19:39
  • 5
    You need the detail to see what I'm talking about. I personally like longer explanations like this since I know exactly what the programmer has already gone through. I find when I don't, people keep posting the exact solutions I've already tried, even when I've already explained why they won't work because they didn't understand *why* they won't work. And simply put, this isn't a basic issue for a 'noob' site that can be reduced to bullet points, hence my posting on SO in the first place. – Mark A. Donohoe May 01 '11 at 19:48
  • 4
    Oh my god... I always thought SO is exempt from the TL;DR rule - after all programmers must be able to read! – Vladislav Zorov May 01 '11 at 20:44
  • Thanks, @Vladislav Zorov! Couldn't agree more! – Mark A. Donohoe May 02 '11 at 04:18
  • LostKeyboardFocus also fires when you switch to a different application which is super annoying. – Paul McCarthy Feb 04 '20 at 14:34
  • (KeyboardFocusChangedEventArgs) e.newFocus is null when switching application so you can check for that. – Paul McCarthy Feb 04 '20 at 14:50
  • 1
    YEs, @PaulMcCarthy, but then you still can't use that as you still have the issue of losing keyboard focus when a context menu pops up. That's why you need the combination of the two. – Mark A. Donohoe Feb 04 '20 at 16:14

6 Answers6

11

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

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
3

Unfortunately you are looking for a simple solution to a complex problem. The problem stated simply is to have smart auto-committing user interface controls that require a minimum of interaction and "do the right thing" when you "switch away" from them.

The reason it is complex is because what the right thing is depends on the context of the application. The approach WPF takes is to give you to logical focus and keyboard focus concepts and to let you decide how to do the right thing for you in your situation.

What if the context menu is opened? What should happen if the application menu is opened? What if the focus is switched to another application? What if a popup is opened belonging to the local control? What if the user presses enter to close a dialog? All these situations can be handled but they all go away if you have a commit button or the user has to press enter to commit.

So you have three choices:

  • Let the control stay in the editing state when it has the logical focus
  • Add an explicit commit or apply mechanism
  • Handle all the messy cases that arise when you try to support auto-commit
Rick Sladkey
  • 33,988
  • 6
  • 71
  • 95
  • 1
    I don't think this behavior is complicated at all. It's simply 'Drop out of edit mode when you lose focus' with the caveat that showing a context menu doesn't constitute losing focus. Windows Explorer has been doing this forever. Rename a file and that's the exact behavior you get... no 'Commit/Cancel' buttons needed. WinForms it was cake too since the context menus didn't steal focus. IMHO, I disagree (although I do understand why) MS made a context menu steal keyboard focus. They should have considered that however. You have to take 7 left turns to go right now. – Mark A. Donohoe May 01 '11 at 22:25
  • @MarqueIV: It's quite complicated actually, even the DataGrid has some problems with it. If you have DataGrids in a DataTemplate of a TabControl and you switch tabs while editing you get an exception because of some edit-logic gone wrong. – H.B. May 01 '11 at 22:30
  • @MarquelIV: But this is not Windows Explorer is it, nor is it WinForms. I'm not defending the whole focus design and infrastructure of WPF; it is what it is. Given that it is what is is you have three choices. – Rick Sladkey May 01 '11 at 22:31
  • But that's the great thing about being a programmer... figuring out how to do something with what you have as opposed to what you want. It's like getting paid to solve puzzles all day, and every now and then you get something really creative and unexpected. That's why I'm asking SO if anyone else has managed to duplicate this behavior and how they got around it. Just because you have a screwdriver and not a hammer doesn't mean you can't use the handle to bang in nails. Optimal? No. But better than just rolling over and saying you can't do it. – Mark A. Donohoe May 01 '11 at 22:38
  • Oh, and @Rick Sladkey, I don't believe I ever once said I believed the answer was simple. Again, wouldn't have come here if it was. SO is for the best of the best to show their stuff, not 'noob' forums with people who read TYS WPF in 21 Days. (Also, what does 'This isn't explorer' have to do with anything? That's learned behaviors of the OS that you want to replicate for consistency. That just makes for a great user experience, just like you instinctively know F2 usually means rename.) – Mark A. Donohoe May 01 '11 at 22:40
  • 1
    @MarqueIV: I apologize for suggesting you were looking for a simple solution. I should have said "we all wish there were a simpler solution". – Rick Sladkey May 01 '11 at 22:49
  • @Rick Sladkey, NP. But back to the question... can you think of how to 'Handle all the messy cases that arise...'? My suggestions look promising? Can you think of any caveats or other avenues here? We really want this behavior since again, people (our customers) are already used to it in both Windows and in our legacy non-WPF apps. – Mark A. Donohoe May 01 '11 at 22:54
  • @MarqueIV: You are on the right track. Yes, I've lost the keyboard focus, but, it's because the context menu is up. I would have to code up a sample to be sure the logic was right. When you get it right your customers won't think twice but you'll know the hoops you had to jump through. – Rick Sladkey May 01 '11 at 23:07
  • Hey @Rick Sladkey ... I see you also commented on my other question on a similar topic about default context menus, which I realized although I marked it as answered, it really wasn't. Specifically, how can I tell when the default context menu for a textbox is about to be displayed? ContextMenuOpening only fires when you explicitly set your own. If I can find that out (short of just creating my own) then I have a solution to the issue in this question. (Even if I can somehow explicitly assign the context menu to its default, that may just work too since ContextMenu won't be null.) – Mark A. Donohoe May 01 '11 at 23:53
  • @MarqueIV: You missed your chance. You had one of the smartest guys in all of WPF-land answering your question for that one. As far as ContextMenuOpening, you can derive from TextBox and override OnContextMenuOpening to hook that event. – Rick Sladkey May 02 '11 at 01:18
  • @Rick Sladkey ... who were you referring to? And what did I miss? And are you sure you can still hook that event even if there isn't a menu? Not sure which would've been the better approach... subclassing the textbox, or styling it like I did. – Mark A. Donohoe May 02 '11 at 03:57
  • 1
    @MarqueIV: Josh Smith is one of the icons of the WPF community. He answered the question you said wasn't really answered. If you ever had a chance to answer your question, that was it. BTW, I know you took some grief for this question so I'm glad you rose above the fray and kept your focus on the problem. – Rick Sladkey May 02 '11 at 04:11
  • Thanks @Rick Sladkey! Truthfully today was my first negative experience on here. The funny thing is @H.W. was also one of the people answering over there so he already knows I tend to write a lot. It's just a style. People don't have to agree. My point was it wasn't relevant to the question, nor furthered it along (as per the FAQ here) so I didn't get the drama. So what! If it's too long, don't read it! That was my take. Different strokes I guess. BTW, even longer than my Q was my A. Did you check it out? Thoughts? I'd love feedback. It feels 'hacky' to me but it does work! – Mark A. Donohoe May 02 '11 at 04:17
1

Wouldn't it be just easier to:

    void txtBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        TextBox txtBox = (sender as TextBox);

        if (e.NewFocus is ContextMenu && (e.NewFocus as ContextMenu).PlacementTarget == txtBox)
        {
            return;
        }

        // Rest of code for existing edit mode here...
    }
Saragani
  • 96
  • 7
  • Unfortunately no, because if you open the context menu, you lose keyboard focus, and you would return. If you then clicked elsewhere in the app, or in another app, you would not have any other notifications to exit edit mode. However, your code may simplify the context menu hack I have above where I re-style the control although the placement target then becomes a factor as it may be set elsewhere. Still, good food for thought. Thanks! – Mark A. Donohoe May 11 '16 at 07:24
0

Im not sure about the context menu issue but I was trying to do something similar and found that using mouse capture gives you (just about) the behaviour you are after:

see the answer here: How can a control handle a Mouse click outside of that control?

Community
  • 1
  • 1
JonnyRaa
  • 7,559
  • 6
  • 45
  • 49
  • The problem with this approach is it doesn't consider keyboard-only navigation. Still, good info for others. Thanks! – Mark A. Donohoe Feb 02 '13 at 08:36
  • Im about to try adding tab nagivation to my controls so may be feeling your pain soon! Are there any ways you can lose focus through the keyboard where you dont get the keypress events? Context menus in wpf are a bit of a nightmare in general - they seem to be riddled with bugs, so it doesnt surprise me that they cause problems here aswell – JonnyRaa Feb 05 '13 at 13:01
  • Sure! Any time you execute a system key (i.e. Alt-Tab) you won't get the notification. Well, you'll get a half-notification as you'll see the 'Alt' but you won't get the 'Tab' when you switch away. However, the approach above (while conviluted) works for those cases too since it relies on both regular LostFocus and keyboard focus so if you follow it, you should be golden even in your case. HTH! – Mark A. Donohoe Feb 06 '13 at 09:55
0

Not sure, but this could be helpful. I had a similar issue with Editable combo box. My problem was I was using OnLostFocus override method which was not getting called. Fix was I had attached a callback to LostFocus event and it worked all fine.

0

I passed through here on my search for a solution for a similar problem: I have a ListBox which loses focus when the ContextMenu opens, and I don't want that to happen.

My simple solution was to set Focusable to False, both for the ContextMenu and its MenuItems:

<ContextMenu x:Key="QueryResultsMenu" Focusable="False">
    <ContextMenu.Resources>
        <Style TargetType="MenuItem">
            <Setter Property="Focusable" Value="False"/>
        </Style>
    </ContextMenu.Resources>
    <MenuItem ... />
</ContextMenu>

Hope this helps future seekers...

TheDark
  • 31
  • 3
  • 1
    Doesn't that break keyboard navigation of the context menus? Remember, they can be launched from the keyboard. – Mark A. Donohoe Jun 24 '15 at 04:46
  • You're right, in my case it doesn't matter because there isn't a keyboard present. – TheDark Jun 30 '15 at 15:31
  • Couldn’t you just set the context menu to an empty collection instead of null? Also, I think there is an event that you can intercept and handle that will stop it from opening in the first place. – Mark A. Donohoe Sep 29 '20 at 16:53