I Have a wpf Listbox that display's a list of textboxes. When I click on the Textbox the Listbox selection does not change. I have to click next to the TextBox to select the listbox item. Is there some property I need to set for the Textbox to forward the click event to the Listbox?
15 Answers
We use the following style to set a PreviewGotKeyboardFocus which handles all events of TextBox control and ComboBoxes and such:
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<EventSetter Event="PreviewGotKeyboardFocus" Handler="SelectCurrentItem"/>
</Style>
</ListView.ItemContainerStyle>
And then we select the row in code behind:
protected void SelectCurrentItem(object sender, KeyboardFocusChangedEventArgs e)
{
ListViewItem item = (ListViewItem) sender;
item.IsSelected = true;
}
I don't have enough reps to comment, so I am posting my comment as answer. Grazer's solution above does not work in cases where you have another control such as a Button
that needs the SelectedItem
. This is because as per the Style Trigger
, the IsKeyboardFocusWithin
becomes false when you click on that Button
, and the SelectedItem
becomes null.
I used similar to Robert's solution, but without code behind (using attached behavior).
To do so,
First. Create separate class FocusBehaviour:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace MyBehaviours
{
public class FocusBehaviour
{
#region IsFocused
public static bool GetIsFocused(Control control)
{
return (bool) control.GetValue(IsFocusedProperty);
}
public static void SetIsFocused(Control control, bool value)
{
control.SetValue(IsFocusedProperty, value);
}
public static readonly DependencyProperty IsFocusedProperty = DependencyProperty.RegisterAttached(
"IsFocused",
typeof(bool),
typeof(FocusBehaviour),
new UIPropertyMetadata(false, IsFocusedPropertyChanged));
public static void IsFocusedPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var control = sender as Control;
if (control == null || !(e.NewValue is bool))
return;
if ((bool)e.NewValue && !(bool)e.OldValue)
control.Focus();
}
#endregion IsFocused
#region IsListBoxItemSelected
public static bool GetIsListBoxItemSelected(Control control)
{
return (bool) control.GetValue(IsListBoxItemSelectedProperty);
}
public static void SetIsListBoxItemSelected(Control control, bool value)
{
control.SetValue(IsListBoxItemSelectedProperty, value);
}
public static readonly DependencyProperty IsListBoxItemSelectedProperty = DependencyProperty.RegisterAttached(
"IsListBoxItemSelected",
typeof(bool),
typeof(FocusBehaviour),
new UIPropertyMetadata(false, IsListBoxItemSelectedPropertyChanged));
public static void IsListBoxItemSelectedPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var control = sender as Control;
DependencyObject p = control;
while (p != null && !(p is ListBoxItem))
{
p = VisualTreeHelper.GetParent(p);
}
if (p == null)
return;
((ListBoxItem)p).IsSelected = (bool)e.NewValue;
}
#endregion IsListBoxItemSelected
}
}
Second. Add a style in resources section (my style is rounded black on focus). Notice setter for FocusBehaviour.IsListBoxItemSelected property. You should reference it in xmlns:behave="clr-namespace:MyBehaviours"
`
<Style x:Key="PreviewTextBox" BasedOn="{x:Null}" TargetType="{x:Type TextBox}">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="1"/>
<Setter Property="AllowDrop" Value="true"/>
<Setter Property="Background" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border
Margin="6,2,0,4"
BorderBrush="#FFBDBEBD"
BorderThickness="1"
CornerRadius="8"
Background="White"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
MinWidth="100"
x:Name="bg">
<ScrollViewer
x:Name="PART_ContentHost"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="Background" TargetName="bg" Value="Black"/>
<Setter Property="Background" Value="Black"/><!-- we need it for caret, it is black on black elsewise -->
<Setter Property="Foreground" Value="White"/>
<Setter Property="behave:FocusBehaviour.IsListBoxItemSelected" Value="True"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
`
Third. (optional, for reverse task)
You will meet, if not any, reverse task - focusing on TextBox when ListBoxItem get selected.
I recommend using another property of Behaviour class, IsFocused. Here is a sample template for ListBoxItem
, please notice Property="behave:FocusBehaviour.IsFocused"
and FocusManager.IsFocusScope="True"
<DataTemplate x:Key="YourKey" DataType="{x:Type YourType}">
<Border
Background="#FFF7F3F7"
BorderBrush="#FFBDBEBD"
BorderThickness="0,0,0,1"
FocusManager.IsFocusScope="True"
x:Name="bd"
MinHeight="40">
<TextBox
x:Name="textBox"
Style="{StaticResource PreviewTextBox}"
Text="{Binding Value}" />
</Border>
<DataTemplate.Triggers>
<DataTrigger
Binding="{Binding IsSelected,RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
Value="True">
<Setter
TargetName="textBox"
Property="behave:FocusBehaviour.IsFocused"
Value="True" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
I use a class handler to set this behavior. Doing it this way will fix all of the list views in the application. I don't know why this is not the default behavior.
In your App.xaml.cs, add the following to OnStartup:
protected override void OnStartup(StartupEventArgs e)
{
EventManager.RegisterClassHandler(typeof (ListViewItem),
ListViewItem.PreviewGotKeyboardFocusEvent,
new RoutedEventHandler((x,_) => (x as ListViewItem).IsSelected = true));
}
Is there some property I need to set for the Textbox to forward the click event to the Listbox?
It's not a simple property, but you can handle the GotFocus
event on your TextBox
, then use VisualTreeHelper to find the ListBoxItem
and select it:
private void TextBox_GotFocus(object sender, RoutedEventArgs e)
{
TextBox myTextBox = sender as TextBox;
DependencyObject parent = VisualTreeHelper.GetParent(myTextBox);
while (!(parent is ListBoxItem))
{
parent = VisualTreeHelper.GetParent(parent);
}
ListBoxItem myListBoxItem = parent as ListBoxItem;
myListBoxItem.IsSelected = true;
}
The simplest way I've been able to find to do this is to use the PreviewMouseDown event and set the IsSelected property of the templated parent. Since the preview events bubble down, the ListBoxItem will process the event as soon as the user clicks the textbox, combobox, or any other control you set the event on.
One nice thing about this is that you can use the same event for all types of controls since they all derive from Framework element. Also, setting IsSelected (instead of setting the SelectedItem) will cause multiple items to be selected when you set the SelectionMode of the listbox to "Extended", which could or could not be what you're looking for.
ie:
c# code
private void Element_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
((sender as FrameworkElement).TemplatedParent as ListBoxItem).IsSelected = true;
}
xaml
...
<ComboBox PreviewMouseDown="Element_PreviewMouseDown"/>
<TextBox PreviewMouseDown="Element_PreviewMouseDown"/>
...
Old discussion, but maybe my answer helps others....
Ben's solution has the same problem as Grazer's solution. The bad thing is that the selection depends on the [keyboard] focus of the textbox. If you have another control on your dialog (i.e. a button) the focus gets lost when clicking the button and the listboxitem becomes un-selected (SelectedItem == null). So you have different behavior for clicking the item (outside the textbox) and clicking in the textbox. This is very tedious to handle and looks very strange.
I'm quite sure there is no pure XAML solution for this. We need code-behind for this. The solution is close to what Mark suggested.
(in my example I use ListViewItem instead of ListBoxItem, but the solution works for both).
Code-behind:
private void Element_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var frameworkElement = sender as FrameworkElement;
if (frameworkElement != null)
{
var item = FindParent<ListViewItem>(frameworkElement);
if (item != null)
item.IsSelected = true;
}
}
with FindParent (taken from http://www.infragistics.com/community/blogs/blagunas/archive/2013/05/29/find-the-parent-control-of-a-specific-type-in-wpf-and-silverlight.aspx ):
public static T FindParent<T>(DependencyObject child) where T : DependencyObject
{
//get parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) return null;
//check if the parent matches the type we're looking for
T parent = parentObject as T;
if (parent != null)
return parent;
return FindParent<T>(parentObject);
}
In my DataTemplate:
<TextBox Text="{Binding Name}"
PreviewMouseDown="Element_PreviewMouseDown"/>
The following is a simplification of @Ben's answer without having to override the DataTemplate. It can even be applied as a static style. Tested with a ListView containing a GridView > GridViewColumn > TextBox
.
Example:
<ListView.Resources>
<Style TargetType="{x:Type ListViewItem}">
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</ListView.Resources>
Attached behavior based on Arcturus Answer to make it reuseable and to have it not hidden in the code behind.
Create file with the attached behavior (= attached property)
public static class SelectListBoxItemWhenControlInsideTheItemIsClickedBehavior
{
public static readonly DependencyProperty EnableProperty = DependencyProperty.RegisterAttached(
"Enable",
typeof(bool),
typeof(SelectListBoxItemWhenControlInsideTheItemIsClickedBehavior),
new FrameworkPropertyMetadata(false, OnEnableChanged));
public static bool GetEnable(FrameworkElement frameworkElement)
{
return (bool)frameworkElement.GetValue(EnableProperty);
}
public static void SetEnable(FrameworkElement frameworkElement, bool value)
{
frameworkElement.SetValue(EnableProperty, value);
}
private static void OnEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ( d is ListBoxItem listBoxItem)
listBoxItem.PreviewGotKeyboardFocus += ListBoxItem_PreviewGotKeyboardFocus;
}
private static void ListBoxItem_PreviewGotKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
{
var listBoxItem = (ListBoxItem)sender;
listBoxItem.IsSelected = true;
}
}
Add it to the resources of your liking.
For example <Windows.Resources>
<Window.Resources>
<Style TargetType="ListViewItem">
<Setter Property="myBehavior:SelectListBoxItemWhenControlInsideTheItemIsClickedBehavior.Enable" Value="true"/>
</Style>
</Window.Resources>
Add namespace
if visual studio is not clever enough to add it automatically. for example when your project is called "MyApp" and you saved the file in the Folder "MyBehaviors", the Namespace would be under Window:
<Window
xmlns:myBehavior="clr-namespace:MyApp.MyBehaviors"
>
I'm not entirely sure you would want to set the selection directly as described in the previous answer because I think it would break multiselection and some other scenerios
. You might want to try restyling a button like below and see what happens.
<Button ClickMode="Pressed" Focusable="False">
<Button.Template>
<ControlTemplate> // change the template to get rid of all the default chrome
<Border Background="Transparent"> // Button won't be clickable without some kind of background set
<ContentPresenter />
</Border>
</ControlTemplate>
</Button.Template>
<TextBox />
Your not very specific about your initial situation. But i assume that you use DataBinding and an ItemTemplate. Thats imho an easy way to do this, as well if your beginner on this topic. This should work:
<ListBox ItemsSource="{Binding someDataCollection}" Name="myListBox">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding datafield}" Tag="{Binding .}"
GotFocus="TextBox_GotFocus"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
private void TextBox_GotFocus(object sender, RoutedEventArgs e)
{
myListBox.SelectedItem = (sender as TextBox).Tag; /* Maybe you need to cast to the type of the objects contained in the collection(bound as ItemSource above) */
}