WPF TabControl - Select different tab when TabItem Visibility changes - wpf

I have a TabControl on a UserControl backed by a ViewModel, and the Visibility of one of the tab items is bound to a property on the ViewModel.
<TabControl x:Name="myTabControl">
<TabItem Header="Tab 1" />
<TabItem Header="Tab 2" Visibility="{Binding HasData, Converter={StaticResource boolToVisibilityConverter}}"/>
</TabControl>
When the Visibility of the TabItem changes, it collapses (hides) the TabItem header, but it continues displaying its content.
I want the TabControl to switch to the visible tab when the other tab is hidden, and was a little surprised to find out it doesn't happen automatically.
Attaching an event handler to the SelectionChanged event of the TabControl shows that TabItem.IsSelected (and TabControl.SelectedItem) is not even affected when the TabItem.Visibility changes (is this a bug?!).
I've tried both a property trigger:
<!-- This doesn't compile because of TargetName on the Setter, think you can only use it in Control Templates.
I don't know how to refer to the parent TabControl from within the TabItem style. -->
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}" BasedOn="{StaticResource {x:Type TabItem}}">
<Style.Triggers>
<Trigger Property="Visibility" Value="Collapsed">
<Setter TargetName="myTabControl" Property="SelectedIndex" Value="0" />
</Trigger>
</Style.Triggers>
</Style>
</TabControl.ItemContainerStyle>
and a data trigger:
<!-- This doesn't quite work, it affects the Visibility of the TabItem's content too -->
<TabControl.Style>
<Style TargetType="{x:Type TabControl}" BasedOn="{StaticResource {x:Type TabControl}}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=SelectedItem.Visibility, ElementName=tabControl}"
Value="Collapsed">
<Setter Property="SelectedIndex" Value="0" />
</DataTrigger>
</Style.Triggers>
</Style>
</TabControl.Style>
I can't get the triggers to work, and there's no VisibilityChanged event I can handle, so I'm kind of stuck and would appreciate some help.

The TabItem class has an IsVisibleChanged event that you can use.

Bind SelectedIndex of TabControl to a property. And change the value of this property to the index of tab which you want to display whenever you change the visibility to collapse of tab item.

You could add this event handler to the code behind. It will test your control in the first place and on changes to tab visibility due to bindings.
Instead of doing this OnLoaded of course it makes total sense to put this into an attached Property. (AutoSelect?) . The code is the same. You get called in the first place and attach events to IsVisibleChanged. Then the only trick is to use a lambda (Parameter binding) to get the TabControl instance into the event callback. I am posting this solution, because it is shorter.
private void FrameworkElement_OnLoaded(object sender, RoutedEventArgs e)
{
var tabControl = (TabControl) sender;
// register visibility changed to react on changes
foreach (TabItem item in tabControl.Items)
{
item.IsVisibleChanged += (mSender, ev) => item_IsVisibleChanged(mSender, ev, tabControl);
}
// if current selected tab is invisible, find and select first visible one.
if (!((TabItem) tabControl.SelectedItem).IsVisible)
{
foreach (TabItem item in tabControl.Items)
{
if (item.IsVisible)
{
tabControl.SelectedItem = item;
return;
}
}
}
}
private static void item_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e, TabControl tabControl)
{
// just became IsVisible = false
if ((bool)e.NewValue == false)
{
if (tabControl == null) return;
ItemCollection items = tabControl.Items;
foreach (UIElement item in items)
{
if (item.IsVisible)
{
tabControl.SelectedItem = item;
return;
}
}
}
}

Related

Setting a local style for an adorner

I have an adorner which should be placed beside it's adorned element. Depening on the value of the custom Position dependency property the adorner appears at the left or right side of the element.
I want to use a style to set the value of the Position property. But I can only do this if I add the style to the resources of the top-level control. If I place the style inside the resources of any child element it shows no effect.
Is there a way that I can set the adorner style on a per-element basis like in the following example?
<Window x:Class="StyledAdorner.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StyledAdorner">
<Window.Resources>
<Style TargetType="local:MyAdorner">
<Setter Property="Position" Value="Right" />
</Style>
<Style TargetType="Button">
<Setter Property="Content" Value="Adorn me!" />
<Setter Property="Margin" Value="15" />
<EventSetter Event="Click" Handler="AddAdorner" />
</Style>
</Window.Resources>
<StackPanel>
<Button />
<Button>
<Button.Resources>
<Style TargetType="local:MyAdorner">
<!-- This setter has no effect! -->
<Setter Property="Position" Value="Left" />
</Style>
</Button.Resources>
</Button>
</StackPanel>
</Window>
The only solution I can image is to scan the adorned element's resources for an adorner style. If there is one then check if there is a setter for the Position property and use this value. But that looks like a really dirty hack...
Code for AddAdorner handler that creates the adorner:
private void AddAdorner(object sender, RoutedEventArgs e)
{
new MyAdorner((UIElement)sender);
}
Constructor for MyAdorner
private Path _indicator = new Path { /* details omitted */ };
public MyAdorner(UIElement adornedElement) : base(adornedElement)
{
AdornerLayer.GetAdornerLayer(AdornedElement).Add(this);
AddVisualChild(_indicator);
InvalidateMeasure();
InvalidateArrange();
}
I could solve my problem by turning the Position property into an attached property:
public static readonly DependencyProperty PositionProperty = DependencyProperty.RegisterAttached(
"Position",
typeof(AdornerPosition),
typeof(MyAdorner),
new FrameworkPropertyMetadata(AdornerPosition.Right, UpdateAdornerLayerLayout));
Now, I just have to set the desired value on the adorned element:
<Button local:MyAdorner.Position="Left">
The property is evaluated in adorner`s ArrangeOverride method when the position for the adorner is calculated.
Note that the UpdateAdornerLayerLayout property changed callback is neccessary to force a layout update for the adorner layer when the property changes:
private static void UpdateAdornerLayerLayout(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is UIElement element)
{
var layer = AdornerLayer.GetAdornerLayer(element);
layer?.Update();
}
}

WPF Listview button selecting item

I have a button in a Listview column. When I press the button, the row does not select. I found one answer to the problem by using:
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True"></Setter>
</Trigger>
</Style.Triggers>
</Style>
This works to select the row, but when the row loses focus it doesn't stay selected, and I need to be able to select multiple rows.
I also tried this solution:
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<EventSetter Event="PreviewGotKeyboardFocus" Handler="SelectCurrentItem"/>
</Style>
</ListView.ItemContainerStyle>
And in code behind:
private void SelectCurrentItem(object sender, KeyboardFocusChangedEventArgs e)
{
ListViewItem item = (ListViewItem) sender;
item.IsSelected = true;
}
This is closer to what I want, but now this affect selecting multiple items and now the user has to click twice on another item before it will select.
I also tried re-selecting the row in the click handler by using:
ListViewItem selectedRow = ((FrameworkElement)sender).DataContext as ListViewItem;
and
((sender as FrameworkElement).TemplatedParent as ListViewItem).IsSelected = true;
but these give me a null error. Any suggestions for what I'm doing wrong?
I have to use a Listview for other reasons, or I would just use a Datagrid, which works great.
Try adding a handler on the button click that reselects the row.

WPF RichTextBox: SelectionOpacity while not focused

Is it possible to make WPF 4.0 RichTextEdit display it's Selection while not focused?
By default, the selection highlighting rectangle disappears when RichTextEdit loses focus.
The following natually does not work.
<RichTextBox>
<RichTextBox.Style>
<Style TargetType="{x:Type RichTextBox}">
<Style.Triggers>
<Trigger Property="IsFocused" Value="false">
<Setter Property="SelectionOpacity" Value="1"/>
</Trigger>
</Style.Triggers>
</Style>
</RichTextBox.Style>
<FlowDocument>
<Paragraph>
<Run>Example Text</Run>
</Paragraph>
</FlowDocument>
</RichTextBox>
You have to handle the LostFocus event to prevent the selection to be removed.
xaml:
<RichTextBox LostFocus="RichTextBox_OnLostFocus" />
code behind:
void RichTextBox_OnLostFocus(object sender, RoutedEventArgs e)
{
var rtb = e.Source as RichTextBox;
if (rtb == null)
return;
if (!rtb.Selection.Start.Equals(rtb.Selection.End))
{
e.Handled = true;
}
}
Old question, but for anyone looking for an answer in future - any controls inheriting from TextBoxBase have a IsInactiveSelectionHighlightEnabled property.
If you set this property as true, selected text will remain highlighted when the containing control has lost focus (though the colour brush used is different).
This should cover WPF RichTextBox or FlowDocumentReader.

WPF setting keyboard-focus without code behind at runtime

I searched a lot on that topic but couldnt really find a solution for this using no code behind. I know some would say using code-behind for this view related things is totally ok, but nevertheless i would like to avoid it.
I have a usercontrol which shows a "dialog" with a single textbox and an OK button. That dialog is a simple usercontrol that is placed on top of all others. By default the usercontrols visibility is set to collapsed. I would like to set the keyboardfocus to the textbox on the dialog usercontrol if the usercontrol gets visible. Is there any way to do this completely in xaml? Since my dialog-control is not visible at the time when the control is loaded, simply setting
FocusManager.FocusedElement="{Binding ElementName=tbID}"
will not work. I tried to use some kind of visibility trigger:
<TextBox Grid.Column="3"
Grid.Row="5"
Name="tbID"
VerticalAlignment="Center">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Visibility" Value="Visible">
<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=tbID}" />
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
but this doesnt work either. The trigger gets fired but the textbox doesnt get the focus. I would really appreciate any suggestions on that. Thanks in advance!
You could try using an attached behavior to set the focus. Here's some sample code:
public static class Focus
{
public static readonly DependencyProperty ShouldFocusWhenVisibleProperty =
DependencyProperty.RegisterAttached("ShouldFocusWhenVisible", typeof (bool), typeof (Focus), new PropertyMetadata(default(bool), ShouldFocusWhenVisibleChanged));
private static void ShouldFocusWhenVisibleChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var uiElement = sender as UIElement;
if (uiElement == null) return;
var shouldFocus = GetShouldFocusWhenVisible(uiElement);
if (shouldFocus)
{
UpdateFocus(uiElement);
uiElement.IsVisibleChanged += UiElementOnIsVisibleChanged;
}
else
uiElement.IsVisibleChanged -= UiElementOnIsVisibleChanged;
}
private static void UiElementOnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var uiElement = sender as UIElement;
if (uiElement == null) return;
UpdateFocus(uiElement);
}
private static void UpdateFocus(UIElement uiElement)
{
if (!uiElement.IsVisible) return;
Keyboard.PrimaryDevice.Focus(uiElement);
}
public static void SetShouldFocusWhenVisible(UIElement uiElement, bool value)
{
uiElement.SetValue(ShouldFocusWhenVisibleProperty, value);
}
public static bool GetShouldFocusWhenVisible(UIElement uiElement)
{
return (bool)uiElement.GetValue(ShouldFocusWhenVisibleProperty);
}
}
Then, you apply the following code to the TextBox in your dialog: <TextBox local:Focus.ShouldFocusWhenVisible="True" />. Note that local: will need to be a reference to the namespace of the Focus class above.
I think you want to bind to the UserControl Visibility property not the TextBox
Example
<UserControl x:Class="WpfApplication7.IconButton"
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="100" d:DesignWidth="200" Name="_this">
<Grid>
<TextBox Name="tbID" Margin="0,12,0,0" VerticalAlignment="Top">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=_this, Path=Visibility}" Value="Visible">
<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=tbID}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</Grid>
</UserControl>

How to use IsKeyboardFocusWithin and IsSelected together?

I have a style defined for my ListBoxItems with a trigger to set a background color when IsSelected is 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>
This style maintains the selected item even when the ListBox and ListBoxItem loses focus, which in my case is an absolute must.
The problem is that I also want the ListBoxItem to be selected when one of its TextBox's child gets focused. To achieve this I add a trigger that sets IsSelected to true when IsKeyboardFocusWithin is true:
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True" />
</Trigger>
When I add this trigger the Item is selected when the focus is on a child TextBox, but the first behaviour disappears. Now when I click outside the ListBox, the item is de-selected.
How can I keep both behaviours?
When your listbox looses focus, it will set selected item to null because of your trigger. You can select on focus using some code behind that will not unselect when you loose focus.
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>
Code behind:
private void OnChildGotFocus(object sender, RoutedEventArgs e)
{
_listBox.SelectedItem = (sender as StackPanel).DataContext;
}
"When I add this trigger the Item is selected when the focus is on a child TextBox, but the first behaviour disappears. Now when I click outside the ListBox, the item is de-selected."
Actually, I don't think it has lost that original behavior. What I suspect is happening is you're clicking directly in the textbox from somewhere else so the underlying ListBoxItem never actually became selected. If it did however, you'd see the selection would still remain after you left as you want.
You can test this by forcing the ListBoxItem to be selected by clicking directly on it (side-note: you should always give it a background, even if just 'transparent' so it can receive mouse clicks, which it won't if it's null) or even just hitting 'Shift-Tab' to set the focus there, back from the textbox.
However, that doesn't solve your issue, which is that the TextBox gets the focus but doesn't let the underlying ListBoxItem know about it.
The two approaches you can use for that are an event trigger or an attached behavior.
The first is an event trigger on the IsKeyboardFocusWithinChanged event where you set 'IsSelected' to true if the keyboard focus changed to true. (Note: Sheridan's answer does a faux-change-notification but it should not be used in cases where you can multi-select in the list because everything becomes selected.) But even an event trigger causes issues because you lose the multi-select behaviors such as toggling or range-clicking, etc.
The other (and my preferred approach) is to write an attached behavior which you set on the ListBoxItem, either directly, or via a style if you prefer.
Here's the attached behavior. Note: You again would need to handle the multi-select stuff if you want to implement that. Also note that although I'm attaching the behavior to a ListBoxItem, inside I cast to UIElement. This way you can also use it in ComboBoxItem, TreeViewItem, etc. Basically any ContainerItem in a Selector-based control.
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);
}
}
...and you simply add this as a property setter in your ListBoxItem's style
<Setter Property="behaviors:AutoSelectWhenAnyChildGetsFocus.Enabled" Value="True" />
This of course assumes you've imported an XML namespace called 'behaviors' that points to the namespace where the class is contained. You can put the class itself in a shared 'Helper' library, which is what we do. That way, everywhere we want it, its a simple property set in the XAML and the behavior takes care of everything else.
I figured out that IsKeyboardFocusWithin is not the best solution.
What I did in this case was to set the style on all of the controls used as DataTemplate to send the GotFocus-event to be handled in code behind. Then, in code behind, I searched up the visual tree (using VisualTreeHelper) to find the ListViewItem and set IsSelected to true. This way it does not "touch" the DataContext and works just with the View elements.
<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;
}

Resources