Как использовать IsKeyboardFocusWithin и IsSelected вместе?

У меня есть стиль, определенный для моего ListBoxItems с триггером для установки цвета фона, когда IsSelected имеет значение True:

    <Style x:Key="StepItemStyle" TargetType="{x:Type ListBoxItem}">
        <Setter Property="SnapsToDevicePixels" Value="true"/>
        <Setter Property="OverridesDefaultStyle" Value="true"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ListBoxItem">
                    <Border Name="Border" Padding="0" SnapsToDevicePixels="true">
                        <ContentPresenter />
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsSelected" Value="True">
                            <Setter TargetName="Border" Property="Background" Value="#40a0f5ff"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Этот стиль сохраняет выбранный элемент, даже когда ListBox и ListBoxItem теряют фокус, что в моем случае абсолютно необходимо. Проблема в том, что я также хочу, чтобы ListBoxItem выбирался, когда фокусируется один из его дочерних элементов TextBox. Для этого я добавляю триггер, который устанавливает IsSelected в true, когда IsKeyboardFocusWithin истинно:

<Trigger Property="IsKeyboardFocusWithin" Value="True">
    <Setter Property="IsSelected" Value="True" />
</Trigger>

Когда я добавляю этот триггер, элемент выбирается, когда фокус находится на дочернем элементе TextBox, но первое поведение исчезает. Теперь, когда я нажимаю за пределами ListBox, выбор элемента отменяется.

Как я могу сохранить оба поведения?


person jpsstavares    schedule 11.06.2010    source источник
comment
Красивое решение только для XAML: stackoverflow.com/a/15383435/419761   -  person l33t    schedule 26.02.2018


Ответы (3)


Когда ваш список теряет фокус, он устанавливает для выбранного элемента значение null из-за вашего триггера. Вы можете выбрать фокус, используя некоторый код, который не отменит выбор, когда вы потеряете фокус.

XAML:

<Window x:Class="SelectedTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="300" Width="300">

    <StackPanel>
        <TextBox Text="Loose focus here" />
        <ListBox Name="_listBox" ItemsSource="{Binding Path=Items}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal" GotFocus="OnChildGotFocus">
                        <TextBox Text="{Binding .}" Margin="10" />
                        <TextBox Text="{Binding .}" Margin="10" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="SnapsToDevicePixels" Value="true"/>
                    <Setter Property="OverridesDefaultStyle" Value="true"/>
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ListBoxItem">
                                <Border Name="Border" SnapsToDevicePixels="true" Background="Transparent">
                                    <ContentPresenter />
                                </Border>
                                <ControlTemplate.Triggers>
                                    <Trigger Property="IsSelected" Value="True">
                                        <Setter TargetName="Border" Property="Background" Value="Red"/>
                                    </Trigger>                                   
                                </ControlTemplate.Triggers>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
    </StackPanel>
</Window>

Код позади:

private void OnChildGotFocus(object sender, RoutedEventArgs e) 
{   
   _listBox.SelectedItem = (sender as StackPanel).DataContext; 
}
person Wallstreet Programmer    schedule 11.06.2010
comment
Большое спасибо! Это именно то, что я искал. - person jpsstavares; 11.06.2010

«Когда я добавляю этот триггер, элемент выбирается, когда фокус находится на дочернем текстовом поле, но первое поведение исчезает. Теперь, когда я щелкаю за пределами списка, элемент отменяется».

На самом деле, я не думаю, что он утратил свое первоначальное поведение. Я подозреваю, что происходит то, что вы щелкаете непосредственно в текстовом поле откуда-то еще, поэтому базовый элемент ListBoxItem никогда не был выбран. Однако, если бы это было так, вы бы увидели, что выбор все равно останется после того, как вы уйдете, как хотите.

Вы можете проверить это, заставив элемент ListBoxItem быть выбранным, щелкнув его непосредственно (примечание: вы всегда должны давать ему фон, даже если он просто «прозрачный», чтобы он мог получать щелчки мыши, чего не будет, если он нулевой ) или даже просто нажав «Shift-Tab», чтобы установить фокус туда, обратно из текстового поля.

Однако это не решает вашу проблему, заключающуюся в том, что TextBox получает фокус, но не позволяет базовому элементу ListBoxItem знать об этом.

Два подхода, которые вы можете использовать для этого, — это триггер события или присоединенное поведение.

Первый — это триггер события IsKeyboardFocusWithinChanged, где вы устанавливаете для IsSelected значение true, если фокус клавиатуры изменился на true. (Примечание: ответ Шеридана делает уведомление об искусственном изменении, но его не следует использовать в случаях, когда вы можете выбрать несколько элементов в списке, потому что все становится выбранным.) Но даже триггер события вызывает проблемы, потому что вы теряете поведение множественного выбора например, переключение или щелчок по диапазону и т. д.

Другой (и мой предпочтительный подход) состоит в том, чтобы написать прикрепленное поведение, которое вы устанавливаете в ListBoxItem, либо напрямую, либо через стиль, если хотите.

Вот прикрепленное поведение. Примечание. Вам снова нужно будет обрабатывать материал с множественным выбором, если вы хотите это реализовать. Также обратите внимание, что хотя я привязываю поведение к ListBoxItem, внутри я привожу к UIElement. Таким образом, вы также можете использовать его в ComboBoxItem, TreeViewItem и т. д. Практически любой ContainerItem в элементе управления на основе селектора.

public class AutoSelectWhenAnyChildGetsFocus
{
    public static readonly DependencyProperty EnabledProperty = DependencyProperty.RegisterAttached(
        "Enabled",
        typeof(bool),
        typeof(AutoSelectWhenAnyChildGetsFocus),
        new UIPropertyMetadata(false, Enabled_Changed));

    public static bool GetEnabled(DependencyObject obj){ return (bool)obj.GetValue(EnabledProperty); }
    public static void SetEnabled(DependencyObject obj, bool value){ obj.SetValue(EnabledProperty, value); }

    private static void Enabled_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var attachEvents = (bool)e.NewValue;
        var targetUiElement = (UIElement)sender;

        if(attachEvents)
            targetUiElement.IsKeyboardFocusWithinChanged += TargetUiElement_IsKeyboardFocusWithinChanged;
        else
            targetUiElement.IsKeyboardFocusWithinChanged -= TargetUiElement_IsKeyboardFocusWithinChanged;
    }

    static void TargetUiElement_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        var targetUiElement = (UIElement)sender;

        if(targetUiElement.IsKeyboardFocusWithin)
            Selector.SetIsSelected(targetUiElement, true);
    }

}

... и вы просто добавляете это как установщик свойств в свой стиль ListBoxItem

<Setter Property="behaviors:AutoSelectWhenAnyChildGetsFocus.Enabled" Value="True" />

Это, конечно, предполагает, что вы импортировали пространство имен XML под названием «behaviors», которое указывает на пространство имен, в котором содержится класс. Вы можете поместить сам класс в общую вспомогательную библиотеку, что мы и делаем. Таким образом, везде, где мы этого хотим, это простое свойство, заданное в XAML, а поведение позаботится обо всем остальном.

person Mark A. Donohoe    schedule 05.10.2012

Я понял, что IsKeyboardFocusWithin не лучшее решение.

Что я сделал в этом случае, так это установил стиль для всех элементов управления, используемых в качестве DataTemplate, чтобы отправить событие GotFocus для обработки в коде позади. Затем, в коде позади, я искал визуальное дерево (используя VisualTreeHelper), чтобы найти ListViewItem и установить IsSelected на true. Таким образом, он не "касается" DataContext и работает только с элементами View.

<Style TargetType="{x:Type Control}" x:Key="GridCellControlStyle">
...
<EventSetter Event="GotFocus" Handler="SelectListViewItemOnControlGotFocus"/>
...

private void SelectListViewItemOnControlGotFocus(object sender, RoutedEventArgs e)
{
var control = (Control)sender;
FocusParentListViewItem(control);
}

private void FocusParentListViewItem(Control control)
{
var listViewItem = FindVisualParent<ListViewItem>(control);
if (listViewItem != null)
    listViewItem.IsSelected = true;
}

public static T FindVisualParent<T>(UIElement element) where T : UIElement
{
UIElement parent = element; 

while (parent != null)
{
    var correctlyTyped = parent as T; 

    if (correctlyTyped != null)
    {
        return correctlyTyped;
    }

    parent = VisualTreeHelper.GetParent(parent) as UIElement;
} 

return null;
}
person tridy    schedule 24.09.2012