How do you hide a ListView Item placeholder when it's DataTemplate child is collapsed?

夙愿已清 提交于 2019-12-07 15:07:03

问题


When the visibility of a CarViewControl is set to collapsed, it still shows a placeholder where it used to be (see screenshot below).

Is there any way to completely hide a ListViewItem when it is Collapsed?

XAML Code

<ScrollViewer>
    <ListView ItemsSource="{Binding CarVM.UserCars}" ShowsScrollingPlaceholders="False">
        <ListView.ItemContainerStyle>
            <Style TargetType="ListViewItem">
                <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            </Style>
        </ListView.ItemContainerStyle>
        <ListView.ItemTemplate>
            <DataTemplate>
                <ctrl:CarViewControl Car="{Binding}" Visibility="{Binding HideCar, Converter={ThemeResource InverseVisConverter}}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ScrollViewer>

In the image above, there are three CarViewControls which are collapsed, followed by one which is not. One is highlighted. I want them to be completely invisible when the content is collapsed.

What I've tried:

  • Setting height of the DataTemplate control to 0 (just to see if it hides the placeholder which had no effect
  • Setting ShowsScrollingPlaceholders to False based on this documentation: MSDN ListView Placeholders

Reason For Collapse Requirement

Within each CarViewControl, a WebView exists which includes a security token (which maintains that the WebView is logged into a specific web site). If you try to pass the WebView by reference, due to what I can only assume are security measures, you lose that security token and must re-login to the site. That is why adding/removing the control from the ObservableCollection will not work in my case.


回答1:


I would say your design is flawed, but I can't fix that; so I'll provide a "workaround."

The issue is that your DataTemplate is collapsing, which is great, but clearly the container it is in doesn't collapse. This won't happen inherently because the parent won't inherit from a child. First realization is every item is wrapped in a ListViewItem and you can observe that from setting your ItemContainerStyle. This leaves you with two solutions (workarounds). You can either set up some triggers on your ListViewItem or you can do something easier like I did--and if you don't mind the UI affects.

My full working application is below. The main point is that you have to edit the layout/behavior of the ListViewItem. In my example, the default values aren't BorderThickeness and Padding isn't "0, 0, 0, 0"... Setting those to 0 will get hide your items completely.

MainWindow.xaml

<Window x:Class="CollapsingListViewItemContainers.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"
        xmlns:local="clr-namespace:CollapsingListViewItemContainers"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BoolToVisConverter"></BooleanToVisibilityConverter>
    </Window.Resources>
    <Grid>
        <StackPanel>
            <Button Content="Disappear Car 3" Click="Button_Click" />
            <ListView ItemsSource="{Binding Cars}">
                <ListView.ItemContainerStyle>
                    <Style TargetType="ListViewItem">
                        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                        <Setter Property="MinHeight" Value="0" />
                        <Setter Property="Padding" Value="0 0 0 0" />
                    </Style>
                </ListView.ItemContainerStyle>
                <ListView.ItemTemplate>
                    <DataTemplate DataType="{x:Type local:Car}">
                        <Grid Visibility="{Binding IsVisible, Converter={StaticResource BoolToVisConverter}}">
                            <StackPanel>
                                <TextBlock Text="{Binding Name}" />
                                <TextBlock Text="{Binding Title}" />
                                <TextBlock Text="{Binding Id}" />
                            </StackPanel>
                        </Grid>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </StackPanel>
    </Grid>
</Window>

MainWindow.xaml.cs

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;

namespace CollapsingListViewItemContainers
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public ObservableCollection<Car> _cars = new ObservableCollection<Car>
                        {
                            new Car("Not yours", "Mine", 1),
                            new Car("Not mine", "Yours", 2),
                            new Car("Not ours", "His", 3),
                            new Car("Not ours", "Hers", 4),
                        };
        public ObservableCollection<Car> Cars
        {
            get
            {
                return _cars;
            }
        }
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Cars[2].IsVisible = !Cars[2].IsVisible;
        }
    }

    public class Car : INotifyPropertyChanged
    {
        private bool _isVisible;
        public bool IsVisible
        {
            get
            {
                return _isVisible;
            }
            set
            {
                _isVisible = value;
                NotifyPropertyChanged("IsVisible");
            }
        }
        public string Name
        {
            get; set;
        }

        public string Title
        {
            get; set;
        }

        public int Id
        {
            get; set;
        }

        public Car(string name, string title, int id)
        {
            Name = name;
            Title = title;
            Id = id;
            IsVisible = true;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void NotifyPropertyChanged(string propertyName)
        {
            if(PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

Edit

Let's be honest, the above was a pretty cheap solution and I wasn't satisfied with it after thinking about it for another 3 minutes. The reason I'm dissatisfied is because you can still select the item if you have access to a keyboard. In the example above, click the first item, "hide" the item(s), then use your mouse and the ListView.SelectedItem will still change.

So below is a quick solution (workaround :D ) to actually remove the item from the list and preventing them from getting focus. Replace the ListView.ItemContainerStyle with this one and change the ActualHeight trigger value to match the values you're seeing. This will change based on OS themes I believe--I'll leave it up to you to test. Lastly, remember the ListViewItem.DataContext is going to be that of an item in the ItemsSource. This means the DataTrigger bound to IsVisible is bound to the Car.IsVisible property.

<ListView.ItemContainerStyle>
    <Style TargetType="ListViewItem">
        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        <Style.Triggers>
            <Trigger Property="ActualHeight" Value="4">
                <Setter Property="Visibility" Value="Collapsed" />
            </Trigger>
            <DataTrigger Binding="{Binding IsVisible}" Value="True">
                <Setter Property="Visibility" Value="Visible" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
</ListView.ItemContainerStyle>

Edit 2 (edited before I even posted the first edit.

Screw it, don't bind visibility of your CarViewControl; You don't need to. You solely need to focus on removing the item itself, and once the item is removed, the containing controls will be removed as well (though you should test this yourself and change IsTabStop and IsFocusable if you can still tab to items in CarViewControl). Also, since using an arbitrary number with the ActualHeight binding isn't very safe, just binding straight to the IsVisible property (or HideCar in your case) and triggering visibility of the ListViewItem should be sufficient.

Finally, here is my final XAML:

<Window x:Class="CollapsingListViewItemContainers.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"
        xmlns:local="clr-namespace:CollapsingListViewItemContainers"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BoolToVisConverter"></BooleanToVisibilityConverter>
    </Window.Resources>
    <Grid>
        <StackPanel>
            <Button Content="Disappear Car 3" Click="Button_Click" />
            <ListView ItemsSource="{Binding Cars}">
                <ListView.ItemContainerStyle>
                    <Style TargetType="ListViewItem">
                        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding IsVisible}" Value="False">
                                <Setter Property="Visibility" Value="Collapsed" />
                            </DataTrigger>
                            <DataTrigger Binding="{Binding IsVisible}" Value="True">
                                <Setter Property="Visibility" Value="Visible" />
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ListView.ItemContainerStyle>
                <ListView.ItemTemplate>
                    <DataTemplate DataType="{x:Type local:Car}">
                        <Grid>
                            <StackPanel>
                                <TextBlock Text="{Binding Name}" />
                                <TextBlock Text="{Binding Title}" />
                                <TextBlock Text="{Binding Id}" />
                            </StackPanel>
                        </Grid>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </StackPanel>
    </Grid>
</Window>



回答2:


Instead of attempting to hide item via the UI, I would remove the item from the View or Collection that the ItemsSource is bound against. It's a cleaner approach, and the UI never has to be concerned about Visibility of an item.

EDIT 1

When a user selects a specific car from the quick select menu, it shows that car and hides the others.

Makes sense; let's call that view "UserCars", and assume the ItemsSource is bound to UserCars.

How about this: change the ItemsSource to be bound to "SelectedCollection". When you want to show UserCars, point SelectedCollection to the UserCars collection.

To limit the set, you'd simply point SelectedCollection to a new SingleCar that you populate with only UserCars.SelectedItem

XAML:

<ListView ItemsSource="{Binding SelectedCollection}" SelectedItem="{Binding SelectedCar}">

ViewModel:

private Car _selectedCar;
public Car SelectedCar { 
  get { return _selectedCar; } 
  set { _selectedCar = value; OnPropertyChanged("SelectedCar"); }
}

private ObservableCollection<Car> _selectedCollection = CarVM.UserCars;
private ObservableCollection<Car> SelectedCollection { 
  get { return _selectedCollection; } 
  set { _selectedCollection = value; OnPropertyChanged("SelectedCollection"); }
}

private ObservableCollection<Car> _originalCollectionForReferenceKeepingOnly;

// called via RelayCommand
public void UserJustSelectedACar()
{
    ObservableCollection<Car> singleItemCollection =  new ObservableCollection<Car>();
    singleItemCollection.Add(SelectedCar);
    _originalCollectionForReferenceKeepingOnly = SelectedCollection;
    SelectedCollection = singleItemCollection;
}

public void ReturnToFullUsedCarsList()
{
    SelectedCollection = _originalCollectionForReferenceKeepingOnly;
}

EDIT 2

It appears that you are trying to make a ListView pretend to be a "Car Details" view by hiding these other items. This is inherently a bad idea; a different UI element bound to the Listview's Selected Car Item would make for a better solution. Since the new detail panel would just be looking at an already-generated Car instance, you wouldn't incur any data hit. Even if you can make this approach work right now, I'm worried you're going to just cause yourself more grief in the future.



来源:https://stackoverflow.com/questions/31768684/how-do-you-hide-a-listview-item-placeholder-when-its-datatemplate-child-is-coll

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!