Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

None focusable ComboBoxItem

Tags:

c#

wpf

I am working with ComboBox elements that often contain very large quantities of data; ~250000 data entries.

This works fine when a ComboBox is set up a little like this.

<ComboBox ItemsSource="{Binding Items}">
    <ComboBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ComboBox.ItemsPanel>
</ComboBox>

However, some custom modifications of the ComboBox I am working with require the ComboBoxItem elements to not be focusable. I achieved this by using a setter in the ComboBox.ItemContainerStyle.

<ComboBox ItemsSource="{Binding Items}">
    <ComboBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ComboBox.ItemsPanel>
    <ComboBox.ItemContainerStyle>
        <Style TargetType="ComboBoxItem">
            <Setter
                Property="Focusable"
                Value="False" />
        </Style>
    </ComboBox.ItemContainerStyle>
</ComboBox>

But there is a problem with this. It works fine until an object has been selected. Then when a user tries to open the ComboBox again, it crashes the program.

My question is, how can the ComboBox be set up so all its ComboBoxItem elements are not focusable, but it does not crash the program.


GIF of Problem


Example Code

XAML

<Window x:Class="FocusableTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="450"
        Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="2*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="2*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Viewbox Stretch="Uniform"
                 Grid.ColumnSpan="3">
            <Label Content="Welcome"
                   FontWeight="Bold"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"/>
        </Viewbox>

        <StackPanel Grid.Row="1"
                    Grid.Column="1">
            <ComboBox ItemsSource="{Binding Items}">
                <ComboBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <VirtualizingStackPanel />
                    </ItemsPanelTemplate>
                </ComboBox.ItemsPanel>
                <ComboBox.ItemContainerStyle>
                    <Style TargetType="ComboBoxItem">
                        <Setter
                            Property="Focusable"
                            Value="False" />
                    </Style>
                </ComboBox.ItemContainerStyle>
            </ComboBox>
        </StackPanel>
    </Grid>
</Window>

C#

using System.Collections.ObjectModel;
using System.Security.Cryptography;
using System.Text;

namespace FocusableTest
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            for (int i = 0; i < 250000; i++)
            {
                Items.Add(GetUniqueKey());
            }
            InitializeComponent();
            DataContext = this;
        }

        public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();

        private static string GetUniqueKey(int maxSize = 20)
        {
            char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
            byte[] data = new byte[1];
            using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
            {
                crypto.GetNonZeroBytes(data);
                data = new byte[maxSize];
                crypto.GetNonZeroBytes(data);
            }
            StringBuilder result = new StringBuilder(maxSize);
            foreach (byte b in data)
            {
                result.Append(chars[b % (chars.Length)]);
            }
            return result.ToString();
        }
    }
}
like image 290
Dan Avatar asked Nov 17 '18 15:11

Dan


3 Answers

I'm not sure how exactly your setup looks like, but I've managed to have a TextBox and a ComboBox with the former retaining focus while selecting items from the latter. The trick was twofold - make the ComboBox not focusable (so it does not steal focus when clicked to open the dropdown), and handle PreviewGotKeyboardFocus on ComboBoxItem to prevent it from gaining focus on hover as an alternative to setting Focusable to false, which apparently is the cause of your issue. Here's the code excerpt:

<StackPanel>
    <TextBox></TextBox>
    <ComboBox ItemsSource="{Binding Items}" Focusable="False">
        <ComboBox.ItemsPanel>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel />
            </ItemsPanelTemplate>
        </ComboBox.ItemsPanel>
        <ComboBox.ItemContainerStyle>
            <Style TargetType="ComboBoxItem">
                <EventSetter Event="PreviewGotKeyboardFocus"
                             Handler="ComboBoxItem_PreviewGotKeyboardFocus" />
            </Style>
        </ComboBox.ItemContainerStyle>
    </ComboBox>
</StackPanel>

And code-behind:

private void ComboBoxItem_PreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
    e.Handled = true;
}
like image 186
Grx70 Avatar answered Oct 09 '22 21:10

Grx70


Maybe it's kind of a hack but it works for me: (XAML Test Project with Framework 4.7.1)

Change your XAML to this:

        <ComboBox ItemsSource="{Binding Items}" x:Name="MyComboBox" DropDownOpened="DropDownWasOpened">
            <ComboBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel />
                </ItemsPanelTemplate>
            </ComboBox.ItemsPanel>
            <ComboBox.ItemContainerStyle>
                <Style TargetType="ComboBoxItem">
                    <Setter Property="Focusable" Value="False" />
                </Style>
            </ComboBox.ItemContainerStyle>
        </ComboBox>

And in code behind add the handler:

    private void DropDownWasOpened(object sender, EventArgs e) {
        var selectedItem = MyComboBox.SelectedItem;
        MyComboBox.SelectedItem = null;

        Dispatcher.BeginInvoke(new Action(() => MyComboBox.SelectedItem = selectedItem));
    }

It even has the advantage that the ComboBox opens with the same scroll position as closed.

like image 21
Markus Avatar answered Oct 09 '22 20:10

Markus


First, lets clarify... Your program is not CRASHING (actual run-time erroring out), but hanging (or apparent hang) after you perform a given action.

In fact, it is NOT a problem with the combobox, but the number of items within it. Yes, you may be using some filtering or searchable context, but the overall hang is the number of elements.

Without changing a thing in your code except the loop of random strings, I dropped the list to 50 and kept increasing double+plus until I could get something more measurable.. 50, 100, 250, 500, 1000, 2000, 5000, etc..

When I drop the count to a reasonable number, it works fine... as soon as I kicked it up to about 1000 entries, the delay was slower, but not hung. At 5000 entries, slower still, but took about 32 seconds to open the drop-down list... This time is on a Dell Alienware machine i7-2.8Ghz, 16gig. After the second time the drop-down list opened, subsequent attempts were quick.

Now, with your list of 250,000+ records, it is probably just choking the system with the load of values for subsequent display purposes.

All this being said and it is not a problem with the combobox, it is definitely more of a redesign implementation that you need. If you can edit your original post (vs just adding comments), you might be able to clarify why you would want the list to have all 250k+ records vs a sub-filtered list. if sub-filtered, maybe repopulating the list based on the underlying filter would be a better choice.

Another caveat... If I first pick an item near the top of the list, let the combobox close, then click to re-open it, it takes about a minute+ (10k records) before re-opening. Then if I pick an entry near the bottom of the list, close it and then re-open, it appears quick. Pick an entry near the top, close it and re-open, it takes long time again.

Obviously over-populating the list is not the way to go and you need to consider alternatives in your UI.

What the problem is, is the reload of the combobox on subsequent attempts to display. For whatever internal reasons going in, the more records you have in the list, the longer the time to rebuild the display.

like image 33
DRapp Avatar answered Oct 09 '22 20:10

DRapp