Хорошо... это было "весело", как в Программистском веселье. Настоящая боль в кистере, чтобы понять, но с приятной широкой улыбкой на лице, что я и сделал. (Пришло время нанести немного 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
для моего элемента управления, я просто добавил новый DP во внешний элемент управления с именем IsEditingContextMenu
, который затем привязал к текстовому полю через внутренний стиль 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 означает «Использовать значение по умолчанию»)
Итак, теперь пользователь может использовать обычное свойство ContextMenu
, когда IsEditing
равно false... они могут использовать IsEditingContextMenu
, когда IsEditing имеет значение true, и если они не указали IsEditingContextMenu
, для текстового поля используется внутреннее значение по умолчанию, которое мы определили. Поскольку контекстное меню текстового поля никогда не может быть нулевым, его ContextMenuOpening
всегда срабатывает, и поэтому логика, поддерживающая такое поведение, работает.
Как я уже сказал... НАСТОЯЩАЯ головная боль, когда я во всем этом разбираюсь, но, черт возьми, у меня нет действительно крутого чувства выполненного долга.
Я надеюсь, что это поможет другим здесь с той же проблемой. Не стесняйтесь отвечать здесь или PM мне с вопросами.
отметка
person
Mark A. Donohoe
schedule
02.05.2011