UWP:使用键盘箭头禁用*特定* ListViewItem 的选择

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

我有一个 AutoSuggestBox。当我输入内容时,它会给我一个 "suggestions" 的列表。该列表是 Popup 中的 ListView。无论哪种方式,我都希望列表中的某些项目被禁用,因此用户无法选择它们。

我做的还不错,我的实现如下:

<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>

我在样式中绑定 属性 时遇到了一些问题。但是现在一切正常,所有 ListViewItems 的 "IsEnabled" 属性 都绑定到其内容中的 属性。所以现在,用户不能用鼠标选择特定的项目。

我的问题是!虽然用户不能用鼠标选择一个项目,但他仍然可以用向上 ↑ 和向下 ↓ 箭头选择它(而且不仅仅是设置它们被选中,而是实际上用 Enter 来选择)。我希望用户跳过禁用的项目(仍然可以使用箭头选择常规项目)。

我搜索了很长时间,我确实找到了一个漂亮的解决方案,将 "Focusable" 属性 从 ListViewItem 绑定到我自己的任何 属性,但它只是 WPF,因为我的 ListViewItem 没有 "Focusable" 属性。

所有可能的 ListViewItem 属性,包括:"AllowFocusOnInteraction"、"IsTabStop"、"AllowFocusWhenDisabled"、"IsHitTestVisible" 和其他逻辑相关的东西都不起作用。

经过几个小时的努力,我找到了解决问题的办法。该解决方案的工作方式与我在问题中发布的有所不同。它不会跳过禁用的项目(在遇到带有箭头键的禁用项目时跳到第一个启用的项目)。相反,它允许用户像其他任何项目一样突出显示禁用的项目,但不允许他使用 "Enter" 键 select 它。无论哪种方式,用户都可以理解该项目被禁用,首先是因为它是灰色的(因为它的 "IsEnabled" 设置为 false),其次,我将禁用的 ItemListView 中的文本前景设为红色.

它不会通过简单地在 KeyDown 方法中捕获 "Enter" 并返回 "without doing nothing" 让用户 select 具有 "Enter" 的项目。问题是从哪里获得所需的 KeyDown 方法。

我有自己的 AutoSuggestBox 样式(主要是原始 Windows' AutoSuggestBox,我不记得有人更改过它),如下所示:

<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>

可以看出,AutoSuggestBox的思路是一个带搜索功能的TextBox,一个与之同级的Popup。根据其上方 TextBox 中的文本,Popup 包含 "suggestions" 的 ListView。他们的名字是 "TextBox" & "SuggestionsList".

TextBox(原名 "TextBox")是您要捕获 "KeyDown" 事件的主要元凶。这个想法是,当您使用 up-down 箭头键浏览建议列表时,您的焦点始终停留在同一个文本框上,而不是列表视图或其他任何地方。

所以我所做的就是在 TextBox(上面样式中的那个)中添加一个 Behavior,如下所示:

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

行为class代码: 我添加了几条评论来进一步解释重要部分

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;
    }
}

这可能不是最简单的解释和解决方案,但问题也不是很简单。如果您遇到这个具体问题,希望您能弄清楚整个想法。如果你不遵循 MVVM,整个问题可以用更简单的方式解决 and/or 不关心质量,但主要思想保持不变。

此外,我在问题中发布的 SetterValueBindingHelper 取自 this 博客。非常感谢它的作者 SuperJMN。