I am attempting to set the focus to the first button in an ItemsControl that is templated and bound to a BindableCollection NamesList in the ViewModel. In short, I have a set of buttons that are displayed, and I want the first one (top-leftmost button) to have Focus when the panel appears.

The section of my XAML that is relevant is:

<StackPanel Visibility="{Binding IsNamesListVisible, Converter={StaticResource visibilityConverter}}">
   <StackPanel.Style>
      <Style>
         <Style.Triggers>
            <DataTrigger Binding="{Binding IsNamesListVisible}" Value="True">
               <Setter Property="FocusManager.FocusedElement" Value="{Binding Path=Items[0], ElementName=NamesList}"/>
            </DataTrigger>
         </Style.Triggers>            
      </Style>
   </StackPanel.Style>
   <ItemsControl x:Name="NamesList" IsTabStop="False" Focusable="False" VerticalAlignment="Top" HorizontalAlignment="Center" MaxWidth="500">
       <ItemsControl.ItemsPanel>
           <ItemsPanelTemplate>
               <WrapPanel Orientation="Horizontal" Focusable="False" />
           </ItemsPanelTemplate>
       </ItemsControl.ItemsPanel>
       <ItemsControl.ItemTemplate>
           <DataTemplate>
               <Button x:Name="NameButton" Content="{Binding Name}" Margin="10,10,0,10" Width="200" Height="100" />
           </DataTemplate>
       </ItemsControl.ItemTemplate>
   </ItemsControl>
   <Button x:Name="OtherButton" Content="Other" Margin="10,10,0,10" Width="200" Height="100" />
</StackPanel>

This StackPanel is one of several that are all contained on the same screen, and is made visible when the NamesList is populated in the ViewModel. (The flag 'IsNamesListVisible' is set to TRUE when 'NamesList' is populated.)
I have tried several different approaches, none of which have yielded any results.

I have tried the approach seen above:

<Setter Property="FocusManager.FocusedElement" Value="{Binding Path=Items[0], ElementName=NamesList}"/>

I have tried:

<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=NamesList[0]}"/>

And:

<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=NamesList}"/>

I have also tried adding a converter (found at: https://social.msdn.microsoft.com/Forums/vstudio/en-US/9551f7c9-a067-4921-9006-fb4ead912e7d/set-focusedelement-to-selected-listboxitem?forum=wpf) to my solution and used it:

Converter:

public class ItemsControlContainerConverter: MarkupExtension, IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        ItemsControl items = (ItemsControl)value;
        return items.ItemContainerGenerator.ContainerFromIndex(System.Convert.ToInt32(parameter));
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

Setter:

FocusManager.FocusedElement="{Binding ElementName=list, Converter={loc:ItemsControlContainerConverter}, ConverterParameter=0}">

At this point, I believe that all of these methods are only valid if my ItemsControl is fully populated at runtime. I have used the trigger with the FocusManager.FocusedElement property many times and it has always worked without issue, but I have never used it with an ItemsControl before.

Perhaps I'm missing something obvious, but after a day of banging my head on the proverbial wall, I don't see it.

As mentioned previously, I need the top, leftmost button to have Focus when this panel becomes visible. Any suggestions are greatly appreciated.

1

There are 1 answers

7
EldHasp On

FocusManager.FocusedElement is an attached property and only accepts UIElement-derived elements that can have focus. You are assigning it a regular CLR element that cannot receive focus.

You need a converter that, according to the first element, will receive the first child from the visual tree of the ItemsControl item.

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Markup;
using System.Windows.Media;

namespace Converters
{
    public class GetElementFromItemsControlContainerConverter : IMultiValueConverter
    {
        #region IValueConverter Members

        public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (values == null 
                || values.Length < 2
                || !(values[0] is ItemsControl itemsControl)
                || values[1] is null)
                return DependencyProperty.UnsetValue;

            var item = itemsControl.ItemContainerGenerator.ContainerFromItem(values[1]);

            if (item == null)
                return DependencyProperty.UnsetValue;

            return VisualTreeHelper.GetChild(item, 0);
        }

        object[] IMultiValueConverter.ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        #endregion

        public static GetElementFromItemsControlContainerConverter Instance { get; } 
            = new GetElementFromItemsControlContainerConverter();
    }

    public class GetElementFromItemsControlContainerConverterExtension : MarkupExtension
    {
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return GetElementFromItemsControlContainerConverter.Instance;
        }
    }
}
<StackPanel Height="600" Width="800">
    <FocusManager.FocusedElement>
        <MultiBinding Converter="{converters:GetElementFromItemsControlContainerConverter}">
            <Binding ElementName="itemsControl"/>
            <Binding ElementName="itemsControl" Path="ItemsSource[0]"/>
        </MultiBinding>
    </FocusManager.FocusedElement>
    <TextBox Text="12345"/>
    <ItemsControl x:Name="itemsControl">
        <ItemsControl.ItemsSource>
            <x:Array Type="{x:Type sys:String}">
                <sys:String>One</sys:String>
                <sys:String>Two</sys:String>
                <sys:String>Three</sys:String>
            </x:Array>
        </ItemsControl.ItemsSource>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Button Content="{Binding}">
                    <Button.Style>
                        <Style TargetType="Button">
                            <Style.Triggers>
                                <Trigger Property="IsFocused" Value="True">
                                    <Setter Property="Background" Value="LightGreen"/>
                                </Trigger>
                            </Style.Triggers>
                        </Style>
                    </Button.Style>
                </Button>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</StackPanel>

Are you able to bind your ItemsSource to a BindableCollection in the ViewModel and still generate the desired behaviour?

Without any problem:

public class DataViewModel
{
    public ObservableCollection<string> Numbers { get; }
        = new ObservableCollection<string>()
        {
            "One",
            "Two",
            "Three"
        };
}
    <StackPanel Height="200" Width="200">
        <StackPanel.DataContext>
            <local:DataViewModel/>
        </StackPanel.DataContext>
        <FocusManager.FocusedElement>
            <MultiBinding Converter="{converters:GetElementFromItemsControlContainerConverter}">
                <Binding ElementName="itemsControl"/>
                <Binding ElementName="itemsControl" Path="ItemsSource[1]"/>
            </MultiBinding>
        </FocusManager.FocusedElement>
        <TextBox x:Name="tBox" Text="12345"/>
        <ItemsControl x:Name="itemsControl"
                      ItemsSource="{Binding Numbers}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Button Content="{Binding}">
                        <Button.Style>
                            <Style TargetType="Button">
                                <Style.Triggers>
                                    <Trigger Property="IsFocused" Value="True">
                                        <Setter Property="Background" Value="LightGreen"/>
                                    </Trigger>
                                </Style.Triggers>
                            </Style>
                        </Button.Style>
                    </Button>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>

You can even change the collection asynchronously, but this will not affect the element selected for focus. In the example, it will always be selected with index [1].

public class DataViewModel
{
    public ObservableCollection<string> Numbers { get; }
        = new ObservableCollection<string>()
        {
            "One",
            "Two",
            "Three"
        };

    private readonly DispatcherTimer timer = new DispatcherTimer();

    public DataViewModel()
    {
        timer.Interval = TimeSpan.FromSeconds(1);
        timer.Tick += (s, e) =>
        {
            Numbers.Insert(0, Numbers[Numbers.Count - 1]);
            Numbers.RemoveAt(Numbers.Count - 1);
        };
        timer.Start();
    }
}