Search code examples
c#listviewuwplistviewitem

UWP: Disable *specific* ListViewItem's selection with keyboard arrows


I have an AutoSuggestBox. When I type something in, it gives me a list of "suggestions". The list is a ListView inside a Popup. Either way, I wanted some of the items in the list to be disabled, so user can't choose them.

I did it just fine, my implementation is as follows:

<AutoSuggestBox x:Name="AutoSuggest" ...
                ItemContainerStyle="{StaticResource 
                MyPopUpListViewItemStyle}"
/>


<Style x:Key="MyPopUpListViewItemStyle" TargetType="ListViewItem">
...
        <Setter Property="helpers:SetterValueBindingHelper.PropertyBinding">
            <Setter.Value>
                <helpers:SetterValueBindingHelper
                    Property="IsEnabled"
                    Binding="{Binding Path=Content.IsItemEnabled, RelativeSource={RelativeSource Self}}" />
            </Setter.Value>
        </Setter>
...
</Style>

I had some problem with binding property inside a style. But everything works fine now, all the ListViewItems' "IsEnabled" property are binded to a property inside its Content. So now, user can't choose specific items with a mouse.

My problem is! Although user can't choose an item with a mouse, he still can choose it with Up ↑ and Down ↓ arrows (and not just set them selected, but actually choose with Enter). I want the user to skip the disabled items (still being able to use the arrows to choose the regular items).

I searched for quite a while, I did find a nice looking solution, to bind "Focusable" property from ListViewItem to whatever property of my own, but it's WPF only, since there's no "Focusable" property for my ListViewItem.

All the possible ListViewItem properties, including: "AllowFocusOnInteraction", "IsTabStop", "AllowFocusWhenDisabled", "IsHitTestVisible" and other logically relevant things didn't work.


Solution

  • I've found a solution to the problem after several hours of struggling. The solution works some way different from what I posted in the question. It doesn't skip on disabled items (skipping to the first enabled item when encountering a disabled one with arrow keys). Instead, it lets the user have a disabled item highlighted just as any other, but it doesn't let him select it with "Enter" key. Either way, the user can understand that the item is disabled, first because it's greyed out (since its "IsEnabled" is set to false), and second, I made the foreground of the text inside a disabled ItemListView be Red in color.

    It doesn't let the user select the item with "Enter" by simply catching the "Enter" in the KeyDown method and returning "without doing nothing". The problem is where to get the needed KeyDown method.

    I have my own Style for AutoSuggestBox (which is mostly the original Windows' AutoSuggestBox, I don't recall anyone changing anything about it), which goes as follows:

    <Style TargetType="AutoSuggestBox">
        ...
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="AutoSuggestBox">
                    <Grid>
                        ...
                        <TextBox x:Name="TextBox" 
                                 ...
                                 />
                        <Popup x:Name="SuggestionsPopup">
                            <Border x:Name="SuggestionsContainer">
                                <Border.RenderTransform>
                                    <TranslateTransform x:Name="UpwardTransform" />
                                </Border.RenderTransform>
                                <ListView
                                    x:Name="SuggestionsList"
                                    ...
                                    >
                                    <ListView.ItemContainerTransitions>
                                        <TransitionCollection />
                                    </ListView.ItemContainerTransitions>
                                </ListView>
                            </Border>
                        </Popup>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    

    As one can see, the idea of AutoSuggestBox is a TextBox with search, and a Popup on the same level with it. The Popup contains a ListView of "suggestions" according to the text in the TextBox above it. They go by names "TextBox" & "SuggestionsList".

    The TextBox (that goes by the original name "TextBox") is the main culprit which you want to catch the "KeyDown" event onto. The idea is that when you go through the list of suggestions with up-down arrow keys, your focus always stays on that same textbox and not on the listview or whatever else.

    So what I've done is added a Behavior to the TextBox (the one in the style above), as follows:

        <TextBox x:Name="TextBox"
                 ... >
            <interactivity:Interaction.Behaviors>
                <TheNamespaceContainingClass:BehaviorClassName />
            </interactivity:Interaction.Behaviors>
        </TextBox>
    

    The Behavior class code: I added several comments to further explain the important parts

    public class BehaviorClassName: DependencyObject, IBehavior
    {
        private TextBox _associatedObject;
        private ListView _listView;
    
        public DependencyObject AssociatedObject
        {
            get
            {
                return _associatedObject;
            }
        }
    
        public void Attach(DependencyObject associatedObject)
        {
            _associatedObject = associatedObject as TextBox;
    
            if (_associatedObject != null)
            {
                _associatedObject.KeyDown -= TextBox_OnKeyDown;
                _associatedObject.KeyDown += TextBox_OnKeyDown;
            }
        }
    
        private void TextBox_OnKeyDown(object sender, KeyRoutedEventArgs e)
        {
            if (_associatedObject != null)
            {
                if (_listView == null)
                {
                    // Gets ListView through visual tree. That's a hack of course. Had to put it here since the list initializes only after the textbox itself
                    _listView = (ListView)((Border)((Popup)((Grid)_associatedObject.Parent).Children[1]).Child).Child;
                }
                if (e.Key == VirtualKey.Enter && _listView.SelectedItem != null)
                {
                    // Here I had to make sure the Enter key doesn't work only on specific (disabled) items, and still works on all the others
                    // Reflection I had to insert to overcome the missing reference to the needed ViewModel
                    if (!((bool)_listView.SelectedItem.GetType().GetProperty("PropertyByWhichIDisableSpecificItems").GetValue(_listView.SelectedItem, null)))
                        e.Handled = true;
                }
            }
        }
    
        public void Detach()
        {
            _associatedObject.KeyDown -= TextBox_OnKeyDown;
        }
    }
    

    It might not be the most simple explanation and solution, but the problem is not quite simple either. Hope that you can figure the whole idea if you encounter this specific problem. The whole problem can be solved in simpler way if you don't follow MVVM and/or don't care about the quality, but the main idea stays the same.

    Also, the SetterValueBindingHelper I posted in the question is taken from this blog. Many thanks to its author, SuperJMN.