-1

In my WinUI3 app I have a ListView control that illustrates the 'current' item in another part of the app. For visual consistency this is done by binding to the SelectedItem property of the ListView. I would like to stop the user from being able to select other items in the ListView, for example by clicking them.

What I've tried so far:

  1. Setting IsItemClickEnabled="False" on the ListView. No apparent effect.
  2. Setting SelectionMode="None" on the ListView. Disables the display of the current item.
  3. Placing a transparent control in front of the ListView. Stops scrolling of the ListView.
  4. Editing the visual state of ListViewItem as per visc's answer here. Disables the display of the current item.
  5. Setting IsHitTestVisible = false on the item containers (as per mm8's answer below). Stops all interaction on the child controls e.g. scrolling of the child ListViews in the example code below.

Does anyone have any other suggestions?

Edit: The challenge seems to be blocking user selection without blocking all interaction. My ListView actually contains other listviews, like the code below. Crucially, I'd like to retain the ability for the user to (horizontally) scroll the child ListView items.

XAML:

       <Grid RowDefinitions="Auto,*">
            <TextBox x:Name="SelectedItemTextBox" Grid.Row="0" />
            <ListView
                Grid.Row="1"
                ItemsSource="{x:Bind Items, Mode=OneWay}"
                SelectedIndex="2"> 

                <ListView.ItemTemplate>
                    <DataTemplate x:DataType="local:MyItem">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="*" />
                            </Grid.RowDefinitions>
                            <TextBlock
                                Grid.Row="0"
                                Margin="0,8,0,0"
                                Text="{x:Bind Name, Mode=OneWay}" />
                            <ListView
                                Grid.Row="1"
                                Margin="4,0,0,4"
                                ItemsSource="{x:Bind SubItems}"
                                ScrollViewer.HorizontalScrollBarVisibility="Auto"
                                ScrollViewer.HorizontalScrollMode="Enabled"
                                ScrollViewer.VerticalScrollMode="Disabled"
                                SelectedIndex="2">

                                <ListView.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <StackPanel Orientation="Horizontal" />
                                    </ItemsPanelTemplate>
                                </ListView.ItemsPanel>

                                <ListView.ItemTemplate>
                                    <DataTemplate x:DataType="x:String">
                                        <TextBlock Text="{x:Bind}" />
                                    </DataTemplate>
                                </ListView.ItemTemplate>

                            </ListView>

                        </Grid>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </Grid>

C#:

    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // Initially set the target outside the dialog
            RootCanvas.Children.Add(TeachingTip);
            TeachingTip.Target = DialogButton;
            TeachingTip.IsOpen = true;

            for (int i = 0; i < 10; i++)
            {
                MyItem item = new() { Name = i.ToString() };
                for (int j = 0; j < 5; j++)
                {
                    item.SubItems.Add($"Sub item {j + 1}");
                }
                Items.Add(item);
            }            
        }

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

    public class MyItem
    {
        public List<string> SubItems { get; } = new();
        public string Name { get; set; }
    }
Siyh
  • 1,747
  • 1
  • 17
  • 24
  • You're essentially trying to prevent a control from doing what it is designed to do. Wouldn't it make more sense to use a different control? Perhaps the ItemsRepeater is more suitable for your needs. – EddieLotter May 06 '23 at 13:51
  • @EddieLotter That is an option, but as I'm aiming for visual consistency with other ListViews I'd hoped disabling one piece of functionality would be simpler then replicating a whole bunch of other aspects – Siyh May 07 '23 at 20:55
  • Perhaps you just need [Scroll viewer controls](https://learn.microsoft.com/en-us/windows/apps/design/controls/scroll-controls). It's unnecessary to violate `ListView` and in the meanwhile cooperate with it. – YangXiaoPo-MSFT May 08 '23 at 03:01
  • *The challenge seems to be blocking clicks, without blocking all interaction.* Have you considered [the space bar or taps](https://learn.microsoft.com/en-us/windows/apps/design/controls/listview-and-gridview#single-selection)? – YangXiaoPo-MSFT May 09 '23 at 06:24
  • @YangXiaoPo-MSFT I phrased that badly-edited to clarify. – Siyh May 10 '23 at 14:19

2 Answers2

1

This will make each ListViewItem un-clickable. The key point is to apply the DefaultListViewItemStyle so you won't drop the default property settings and default VisualStates.

This is the code of the sample app I created to check this answer. This works including the scrolling.

MainWindow.xaml

<Window
    x:Class="ListViewExample.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:local="using:ListViewExample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid RowDefinitions="Auto,*">
        <TextBox
            x:Name="SelectedItemTextBox"
            Grid.Row="0" />
        <ListView
            Grid.Row="1"
            ItemsSource="{x:Bind Items}"
            SelectedItem="{x:Bind SelectedItemTextBox.Text, Mode=OneWay}">
            <ListView.Resources>
                <Style
                    BasedOn="{StaticResource DefaultListViewItemStyle}"
                    TargetType="ListViewItem">
                    <Setter Property="IsHitTestVisible" Value="False" />
                </Style>
            </ListView.Resources>
        </ListView>
    </Grid>

</Window>

MainWindow.xaml.cs

using Microsoft.UI.Xaml;
using System.Collections.ObjectModel;

namespace ListViewExample;

public sealed partial class MainWindow : Window
{
    public MainWindow()
    {
        this.InitializeComponent();

        for (int i = 0; i < 100; i++)
        {
            Items.Add($"{i + 1}");
        }
    }

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

UPDATE

Instead of using ListView, you can create a custom ItemsControl and use ListViewItem for each item so you can have the Selected look:

CustomItemsControl.cs

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

namespace ListViewExSampleApp;

public class CustomItemsControl : ItemsControl
{
    public static readonly DependencyProperty SelectedIndexProperty =
        DependencyProperty.Register(
            nameof(SelectedIndex),
            typeof(int),
            typeof(CustomItemsControl),
            new PropertyMetadata(-1, OnSelectedIndexPropertyChanged));

    public int SelectedIndex
    {
        get => (int)GetValue(SelectedIndexProperty);
        set => SetValue(SelectedIndexProperty, value);
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        ListViewItem container = new();

        container.Loaded += (sender, e) =>
        {
            if (sender is ListViewItem listViewItem &&
                listViewItem.DataContext is object item &&
                Items.IndexOf(item) == this.SelectedIndex)
            {
                listViewItem.IsSelected = true;
            }
        };

        return container;
    }

    private static void OnSelectedIndexPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is CustomItemsControl selectableItemsControl &&
            e.NewValue is int newIndex &&
            e.OldValue is int oldIndex)
        {
            if (selectableItemsControl.ContainerFromIndex(oldIndex) is ListViewItem oldListViewItem)
            {
                oldListViewItem.IsSelected = false;
            }

            if (selectableItemsControl.ContainerFromIndex(newIndex) is ListViewItem newListViewItem)
            {
                newListViewItem.IsSelected = true;
            }
        }
    }
}

and use it like this:

<ScrollViewer>
    <local:CustomItemsControl
    ItemsSource="{x:Bind ViewModel.Items, Mode=OneWay}"
    SelectedIndex="2">
        <ItemsControl.ItemTemplate>
            <DataTemplate x:DataType="local:Item">
                <ScrollViewer
                HorizontalScrollBarVisibility="Visible"
                HorizontalScrollMode="Enabled"
                VerticalScrollMode="Disabled">
                    <local:CustomItemsControl
                    ItemsSource="{x:Bind Children}"
                    SelectedIndex="3">
                        <local:CustomItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <StackPanel Orientation="Horizontal" />
                            </ItemsPanelTemplate>
                        </local:CustomItemsControl.ItemsPanel>
                        <local:CustomItemsControl.ItemTemplate>
                            <DataTemplate x:DataType="local:Item">
                                <TextBlock Text="{x:Bind Name}" />
                            </DataTemplate>
                        </local:CustomItemsControl.ItemTemplate>
                    </local:CustomItemsControl>
                </ScrollViewer>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </local:CustomItemsControl>
</ScrollViewer>

This CustomItemsControl let you bind to its SelectedIndex but it won't change its selection by clicks.

The caveat is that ItemsControl doesn't have scrolling built-in. You need to wrap it with ScrolViewer but shouldn't be a problem.

Andrew KeepCoding
  • 7,040
  • 2
  • 14
  • 21
0

You could for example handle the ContainerContentChanging event and set the IsHitTestVisible property of the containers to false:

private void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
{
    ListViewItem lvi = args.ItemContainer as ListViewItem;
    lvi.IsHitTestVisible = false;
}
mm8
  • 163,881
  • 10
  • 57
  • 88
  • Thanks mm8-unfortunately this also disables scrolling... – Siyh May 06 '23 at 09:04
  • Scrolling of the `ListView`? No, it doesn't. I tested it. You can scroll the `ListView`. – mm8 May 10 '23 at 13:44
  • You are correct. I'd oversimplified the question (now clarified) and had attempted to scroll the sub-lists. – Siyh May 10 '23 at 14:21
  • Maybe you should ask a new question then since the original one was already answered? – mm8 May 10 '23 at 14:21
  • Your answer provides a workaround (disabling cursor interaction on list items) to the problem posed by the original question (how can you prevent the user selecting items by clicking). The workaround may solve some similar scenarios, but as it is a workaround on balance I think it is better to edit the existing question, in case there is a solution that can disable user selection of items. – Siyh May 10 '23 at 14:34
  • "Workaround on balance"? It's a working solution proposal to your original request of "I would like to stop the user from being able to select other items in the ListView, for example by clicking them". What more can you ask for? – mm8 May 10 '23 at 14:36
  • It does disable user selection of items by the way. It's unclear what you mean by workaround. Is there a difference between a workaround and a solution or what's your point? – mm8 May 10 '23 at 14:36
  • https://www.merriam-webster.com/dictionary/work-around In this case, disabling all interaction when only one bit of interaction was to be disabled – Siyh May 13 '23 at 08:14