How do you solve the problem with LostFocus / LostKeyboardFocus?

Ok, I have a control that has the IsEditing property, which for the argument has a default template, which is usually a text block, but when IsEditing is true, it swaps the text box for in-place editing. Now that the control loses focus, if it is still editing, it should exit edit mode and change it back in the TextBlock template. Pretty straightforward, right?

Think about the file renaming behavior in Windows Explorer or on the desktop (this is the same as I know ...) This is the behavior we want.

The problem is that you cannot use the LostFocus event, because when you switch to another window (or an element that is FocusManager), LostFocus does not fire, since the control still has a logical focus, so it doesn’t will work.

If you use LostKeyboardFocus, while this fixes the “other FocusManager” problem, now you have a new one: when you edit and you right-click on the text field to display the context menu, because the context menu now has keyboard focus. your control loses keyboard focus, exits editing mode and closes the context menu, confusing the user!

Now I tried to set a flag to ignore LostKeyboardFocus just before opening the menu, and then use this fiag in the LostKeyboardFocus event to determine how to throw it out of edit mode or not, but if the menu is open and I click elsewhere in the application, since the element itself the control no longer had keyboard focus (it was on the menu), the control never receives another LostKeyboardFocus event, so it remains in edit mode. (I may have to add a check when the menu closes to see what has the focus, and then manually remove it from EditMode if it is not a control. This seems promising.)

So ... does anyone know how I can successfully code this behavior?

Mark

+8
wpf lostfocus lost-focus focusmanager keyboardfocusmanager
source share
6 answers

Well ... it was "fun", like a fun program. The real pain in the keester to understand, but with a nice huge smile on my face that I did. (Time to get some IcyHot for my shoulder, given that I patted myself along the way: P)

In any case, this is a multi-stage thing, but it is surprisingly simple as soon as you find out. A short option - you need to use both LostFocus and LostKeyboardFocus , and not one or the other.

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

Context menus and lost keyboard focus

LostKeyboardFocus bit more complicated, because the context menu for your control can launch this 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, therefore, LostKeyboardFocus fire .)

To handle this behavior, you override ContextMenuOpening (or handle the event) and set a class-level flag to indicate that the menu opens. (I use bool _ContextMenuIsOpening .) Then, in the LostKeyboardFocus redefinition (or event), you check this flag, and if it is set, you simply clear it and do nothing. However, if it is not installed, it means that something other than opening the context menu causes the control to lose keyboard focus, so in this case you want to set IsEditing to false.

Already open context menus

Now a strange behavior occurs that, if the context menu for the 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 element The control will first focus on the keyboard, but only for a split second, then it instantly returns it to the new control.

This is really beneficial for us, because it means that we will also get another LostKeyboardFocus event, but this time the _ContextMenuOpening flag will be set to false, and as described above, our LostKeyboardFocus handler LostKeyboardFocus set IsEditing to false, which is what we want. I love serendipity!

Now the focus just shifted to the control you clicked on, without first setting the focus back to the control that owns the context menu, then we will need to do something like connecting the ContextMenuClosing event and checking what control will be in order to get focus further, then we would only IsEditing to false, if the coordinated control was not the one that generated the context menu, so we basically dodged the bullet.

Caution: default context menus

Now also the warning that if you use something like a text field and do not explicitly set your own context menu, then you will not receive the ContextMenuOpening event, which surprised me. This can be easily eliminated by simply creating a new context menu with the same standard commands as the default context menu (for example, cut, copy, paste, etc.) and assign it to the text box. It looks exactly the same, but now you get the event you need to set the flag.

However, even there you have a problem, as if you were creating a control accessible to third-party developers, and the user of this control wants to have their own context menu, you can accidentally set your own higher priority, ll redefine them!

While the text box is actually an element in the IsEditing template for my control, I just added a new DP on the external IsEditingContextMenu control, which then binds to the text box through an internal TextBox , then I added a DataTrigger to this style, which checks the value of IsEditingContextMenu on the external control, and if it is null, I set the default menu that I just created above, which is stored in the resource.

Here's the internal style for the text box (an element named "Root" is an external control that the user actually inserts into his 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> 

Please note that you must set the binding of the initial context menu in the style, and not directly in the text field, otherwise the DataTrigger style will be replaced by the directly set value, which will cause the trigger to be useless and you will return to the square if the person uses "null "for the context menu. (If you want to suppress the menu, you still will not use “zero.” You set it to an empty menu, since null means “Use the default value”)

So now the user can use the regular ContextMenu property when IsEditing is false ... they can use IsEditingContextMenu when IsEditing is true, and if they did not specify IsEditingContextMenu , then the internal default that we defined is used for the text field. Since the context menu of a text field can never be null, its ContextMenuOpening always fires, and so the logic to support this behavior works.

Like I said ... REAL pain in strength can understand all this, but hell if I don't have a really cool feeling of success here.

Hope this helps others here with the same issue. Feel free to answer here or ask me questions.

Mark

+8
source share

Unfortunately, you are looking for a simple solution to a complex problem. The problem is to have intelligent, automatic user interface controls that require minimal interaction and do the right thing when you turn them off.

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

What to do if the context menu is open? What happens if the application menu opens? What if the focus switches to another application? What if a popup is opened from a local control? What if the user presses the enter button to close the dialog? All these situations can be handled, but they all go away if you have a commit button or the user must press enter to commit.

So, you have three options:

  • Let the control remain in edit state when it has logical focus
  • Add an explicit fixation or application mechanism
  • Handle all erratic incidents when trying to support auto-commit
+3
source share

I'm not sure about the context menu problem, but I tried to do something like this and found that using mouse capture gives you (approximately) the behavior you are doing:

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

0
source share

Not sure, but it can be useful. I had a similar problem with an Editable combo box. My problem is that I used the OnLostFocus override method that was not called. It was fixed that I connected the callback to the LostFocus event, and it worked perfectly.

0
source share

I went here when I was looking for a solution for a similar problem: I have a ListBox that loses focus when opening ContextMenu , and I don't want this to happen.

My simple solution was to set Focusable to False for both ContextMenu and MenuItem s:

 <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 ...

0
source share

Wouldn't it be easier:

  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... } 
0
source share

All Articles