Set ListBoxItem.IsSelected when child TextBox is Focused
Asked Answered
D

5

14

I have a typical MVVM scenario: I have a ListBox that is binded to a List of StepsViewModels. I define a DataTemplate so that StepViewModels are rendered as StepViews. The StepView UserControl have a set of labels and TextBoxs.

What I want to do is to select the ListBoxItem that is wrapping the StepView when a textBox is focused. I've tried to create a style for my TextBoxs with the following trigger:

<Trigger Property="IsFocused" Value="true">
    <Setter TargetName="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}" Property="IsSelected" Value="True"/>
</Trigger>

But I get an error telling me that TextBoxs don't have an IsSelected property. I now that but the Target is a ListBoxItem. How can I make it work?

Decagon answered 2/6, 2010 at 17:41 Comment(2)
Can you give the xaml code that describes the entire structure?(textbox,listbox)Ician
I've just posted solution that worked for me: #15367306Traipse
C
33

There is a read-only property IsKeyboardFocusWithin that will be set to true if any child is focused. You can use this to set ListBoxItem.IsSelected in a Trigger:

<ListBox ItemsSource="{Binding SomeCollection}" HorizontalAlignment="Left">
    <ListBox.ItemContainerStyle>
        <Style TargetType="{x:Type ListBoxItem}">
            <Style.Triggers>
                <Trigger Property="IsKeyboardFocusWithin" Value="True">
                    <Setter Property="IsSelected" Value="True" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </ListBox.ItemContainerStyle>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBox Width="100" Margin="5" Text="{Binding Name}"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
Clair answered 3/6, 2010 at 2:27 Comment(2)
There is one really big "gotcha" with this approach -- when your application itself loses focus, IsSelected will be set to false. That is, I click the textbox inside a listbox item but switch to another app (say to answer a StackOverflow question in my browser), then switch back to your application... the IsSelected property will be set to true, false, true, as opposed to just staying true the whole time. This can pose a very big problem if you're driving behaviors off of the ListBox's SelectedItem property.Recognizee
@Recognizee Yep, this nailed me too. If anything else gets focus (even some other control within the WPF app), the ListBoxItem gets unselected. This answer solves it: https://mcmap.net/q/827315/-wpf-setting-isselected-for-listbox-when-textbox-has-focus-without-losing-selection-on-lostfocusLoriannlorianna
P
5

As Jordan0Day correctly pointed out there can be indeed big problems using IsKeyboardFocusWithin solution. In my case a Button in a Toolbar which regards to the ListBox was also not working anymore. The same problem with focus. When clicking the button the ListBoxItem does loose the Focus and the Button updated its CanExecute method, which resulted in disabling the button just a moment before the button click command should be executed.

For me a much better solution was to use a ItemContainerStyle EventSetter as described in this post: ListboxItem selection when the controls inside are used

XAML:

<Style x:Key="MyItemContainer.Style" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Background" Value="LightGray"/>
    <EventSetter Event="GotKeyboardFocus" Handler="OnListBoxItemContainerFocused" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <Border x:Name="backgroundBorder" Background="White">
                    <ContentPresenter Content="{TemplateBinding Content}"/>
                </Border>
            <ControlTemplate.Triggers>
                 <Trigger Property="IsSelected" Value="True">
                     <Setter TargetName="backgroundBorder" Property="Background" Value="#FFD7E6FC"/>
                 </Trigger>
             </ControlTemplate.Triggers>
         </ControlTemplate>
     </Setter.Value>
 </Setter>
</Style>

EventHandler in the code behind of the view:

private void OnListBoxItemContainerFocused(object sender, RoutedEventArgs e)
{
    (sender as ListBoxItem).IsSelected = true;
}
Phone answered 14/1, 2013 at 9:41 Comment(1)
This is the correct way to do it. Should look at the linked social.MSDN post from Dr.WPF too.Tao
F
2

One way to achieve that is by implementing a custom behavior using an attached property. Basically, the attached property would be applied to the ListBoxItem using a style, and would hook up to their GotFocus event. That even fires if any descendant of the control gets the focus, so it is suitable for this task. In the event handler, IsSelected is set to true.

I wrote up a small example for you:

The Behavior Class:

public class MyBehavior
{
    public static bool GetSelectOnDescendantFocus(DependencyObject obj)
    {
        return (bool)obj.GetValue(SelectOnDescendantFocusProperty);
    }

    public static void SetSelectOnDescendantFocus(
        DependencyObject obj, bool value)
    {
        obj.SetValue(SelectOnDescendantFocusProperty, value);
    }

    public static readonly DependencyProperty SelectOnDescendantFocusProperty =
        DependencyProperty.RegisterAttached(
            "SelectOnDescendantFocus",
            typeof(bool),
            typeof(MyBehavior),
            new UIPropertyMetadata(false, OnSelectOnDescendantFocusChanged));

    static void OnSelectOnDescendantFocusChanged(
        DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ListBoxItem lbi = d as ListBoxItem;
        if (lbi == null) return;
        bool ov = (bool)e.OldValue;
        bool nv = (bool)e.NewValue;
        if (ov == nv) return;
        if (nv)
        {
            lbi.GotFocus += lbi_GotFocus;
        }
        else
        {
            lbi.GotFocus -= lbi_GotFocus;
        }
    }

    static void lbi_GotFocus(object sender, RoutedEventArgs e)
    {
        ListBoxItem lbi = sender as ListBoxItem;
        lbi.IsSelected = true;
    }
}

The Window XAML:

<Window x:Class="q2960098.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Height="350" Width="525" xmlns:my="clr-namespace:q2960098">
    <Window.Resources>
        <DataTemplate x:Key="UserControlItemTemplate">
            <Border BorderBrush="Black" BorderThickness="5" Margin="10">
                <my:UserControl1/>
            </Border>
        </DataTemplate>
        <XmlDataProvider x:Key="data">
            <x:XData>
                <test xmlns="">
                    <item a1="1" a2="2" a3="3" a4="4">a</item>
                    <item a1="a" a2="b" a3="c" a4="d">b</item>
                    <item a1="A" a2="B" a3="C" a4="D">c</item>
                </test>
            </x:XData>
        </XmlDataProvider>
        <Style x:Key="MyBehaviorStyle" TargetType="ListBoxItem">
            <Setter Property="my:MyBehavior.SelectOnDescendantFocus" Value="True"/>
        </Style>
    </Window.Resources>
    <Grid>
        <ListBox ItemTemplate="{StaticResource UserControlItemTemplate}"
                 ItemsSource="{Binding Source={StaticResource data}, XPath=//item}"
                 HorizontalContentAlignment="Stretch"
                 ItemContainerStyle="{StaticResource MyBehaviorStyle}">

        </ListBox>
    </Grid>
</Window>

The User Control XAML:

<UserControl x:Class="q2960098.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <UniformGrid>
        <TextBox Margin="10" Text="{Binding XPath=@a1}"/>
        <TextBox Margin="10" Text="{Binding XPath=@a2}"/>
        <TextBox Margin="10" Text="{Binding XPath=@a3}"/>
        <TextBox Margin="10" Text="{Binding XPath=@a4}"/>
    </UniformGrid>
</UserControl>
Fungible answered 2/6, 2010 at 20:40 Comment(2)
Thanks for your answer but Bowen's answer does the job with much less code. Thanks a lot for the help though!Decagon
Indeed, I was not aware of that property, there are so many :) +1 to his answer as wellFungible
D
1

If you create a User Control and then use it as the DataTemplate It seems to work cleaner. Then you don't have to use the dirty Style Triggers that Don't work 100% of the time.

Dunagan answered 26/10, 2012 at 18:54 Comment(0)
B
1

Edit: Someone else already had the same answer on a different question: https://mcmap.net/q/827316/-select-listboxitem-if-textbox-in-itemtemplate-gets-focus

Continuing on Maexs' answer, using an EventTrigger instead of an EventSetter removes the need for code-behind:

<Style.Triggers>
    <EventTrigger RoutedEvent="GotKeyboardFocus">
        <BeginStoryboard>
            <Storyboard >
                <BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="IsSelected" >
                    <DiscreteBooleanKeyFrame Value="True" KeyTime="0:0:0"/>
                </BooleanAnimationUsingKeyFrames>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Style.Triggers>
Betti answered 16/10, 2013 at 13:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.