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