我的 ListView 两次显示相同的项目。我如何解决它?

Posted

技术标签:

【中文标题】我的 ListView 两次显示相同的项目。我如何解决它?【英文标题】:My ListView is showing the same item twice. How do I fix it? 【发布时间】:2020-06-27 22:02:23 【问题描述】:

我有一个ComboBox,它允许用户选择一个类别,还有一个ListView,它绑定到所选类别中的一个ObservableCollection。当用户选择不同的类别时,集合中的项目会更新。有时这会按预期工作,但有时项目列表会被破坏。 当应该有两个单独的项目时,它会显示一个重复的项目。

结果似乎取决于我从哪个类别切换。例如,如果我从没有项目的类别切换到有两个项目的类别,相同的项目会显示两次。但是,如果我从一个包含四个项目的类别切换到包含两个项目的同一类别,它们就会正确显示。

这是一个再现:

MainPage.xaml

<Page
    x:Class="ListViewDuplicateItem_Binding.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ListViewDuplicateItem_Binding">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <ComboBox
            Grid.Row="0"
            Grid.Column="0"
            ItemsSource="Binding ViewModel.Groups"
            SelectedItem="Binding ViewModel.SelectedGroup, Mode=TwoWay" />
        <ListView
            Grid.Row="1"
            Grid.Column="0"
            ItemsSource="Binding ViewModel.Widgets"
            SelectedItem="Binding ViewModel.SelectedWidget, Mode=TwoWay">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:Widget">
                    <TextBlock Text="Binding Id" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <local:MyControl
            Grid.Row="1"
            Grid.Column="1"
            Text="Binding ViewModel.SelectedWidget.Id, Mode=OneWay" />
    </Grid>
</Page>

MainPage.xaml.cs

using Windows.UI.Xaml.Controls;

namespace ListViewDuplicateItem_Binding

    public sealed partial class MainPage : Page
    
        public MainPage()
        
            InitializeComponent();
            DataContext = this;
        

        public MainViewModel ViewModel  get;  = new MainViewModel();
    

MainViewModel.cs

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;

namespace ListViewDuplicateItem_Binding

    public class MainViewModel : INotifyPropertyChanged
    
        private string _selectedGroup;
        private Widget _selectedWidget;

        public MainViewModel()
        
            PropertyChanged += HomeViewModel_PropertyChanged;
            SelectedGroup = Groups.First();
        

        public event PropertyChangedEventHandler PropertyChanged;

        public ObservableCollection<string> Groups  get;  = new ObservableCollection<string>(DataSource.AllGroups);

        public string SelectedGroup
        
            get => _selectedGroup;
            set
            
                if (_selectedGroup != value)
                
                    _selectedGroup = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedGroup)));
                
            
        

        public Widget SelectedWidget
        
            get => _selectedWidget;
            set
            
                if (_selectedWidget != value)
                
                    _selectedWidget = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedWidget)));
                
            
        

        public ObservableCollection<Widget> Widgets  get;  = new ObservableCollection<Widget>();

        private void HomeViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
        
            if (e.PropertyName == nameof(SelectedGroup))
            
                var widgetsToLoad = DataSource.GetWidgetsForGroup(SelectedGroup);
                // Add widgets in this group
                widgetsToLoad.Except(Widgets).ToList().ForEach(w => Widgets.Add(w));
                // Remove widgets not in this group
                Widgets.Except(widgetsToLoad).ToList().ForEach(w => Widgets.Remove(w));
                // Select the first widget
                if (SelectedWidget == null && Widgets.Any())
                
                    SelectedWidget = Widgets.First();
                
            
        
    

DataSource.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace ListViewDuplicateItem_Binding

    public static class DataSource
    
        public static ObservableCollection<string> AllGroups  get;  = new ObservableCollection<string>
        
            "First Widget",
            "First Two Widgets",
            "Last Two Widgets",
            "All Widgets",
            "None"
        ;

        public static List<Widget> AllWidgets  get;  = new List<Widget>
        
            new Widget()
            
                Id = 1,
            ,
            new Widget()
            
                Id = 2,
            ,
            new Widget()
            
                Id = 3,
            ,
            new Widget()
            
                Id = 4,
            
        ;

        public static List<Widget> GetWidgetsForGroup(string group)
        
            switch (group)
            
                case "First Widget":
                    return new List<Widget>  AllWidgets[0] ;

                case "First Two Widgets":
                    return new List<Widget>  AllWidgets[0], AllWidgets[1] ;

                case "Last Two Widgets":
                    return new List<Widget>  AllWidgets[2], AllWidgets[3] ;

                case "All Widgets":
                    return new List<Widget>(AllWidgets);

                default:
                    return new List<Widget>();
            
        
    

Widget.cs

namespace ListViewDuplicateItem_Binding

    public class Widget
    
        public int Id  get; set; 
    

MyControl.xaml

<UserControl
    x:Class="ListViewDuplicateItem_Binding.MyControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <TextBox Text="x:Bind Text, Mode=TwoWay" />
</UserControl>

MyControl.xaml.cs

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace ListViewDuplicateItem_Binding

    public sealed partial class MyControl : UserControl
    
        public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(MyControl), new PropertyMetadata(null));

        public MyControl()
        
            InitializeComponent();
        

        public string Text
        
            get  return (string)GetValue(TextProperty); 
            set  SetValue(TextProperty, value); 
        
    

【问题讨论】:

【参考方案1】:

这似乎仅在项目包含使用绑定标记的自定义控件时发生。

在上面的示例中,如果从MainPage.xaml 中删除MyControl,它会按预期工作。

同样,如果将&lt;local:MyControl Text="Binding ViewModel.SelectedWidget.Id" /&gt; 更改为&lt;local:MyControl Text="x:Bind ViewModel.SelectedWidget.Id" /&gt;,该示例将按预期工作

这似乎是ListView 控件中的一个错误,但您可以使用x:Bind compiled bindings 解决它。

编辑:经过进一步调查,自定义控件可能是红鲱鱼。将自定义控件更改为标准 TextBox 并不能解决我之前认为的问题。该问题可以在没有自定义控件的情况下重现。尽管如此,使用 x:Bind 或完全删除控件确​​实可以解决这种情况下的问题。

【讨论】:

【参考方案2】:

更新我的项目以使用x:Bind (compiled bindings) 似乎解决了这个问题,但一周后我意外地开始在我的ListView 中再次看到重复的项目。这次我发现了导致此问题的其他三个因素。

    我在绑定到SelectedItemTextBoxes 中添加了FallbackValue,以便在未选择任何项目时将其清除。如果我删除 FallbackValue,列表项不会重复。不过,我需要这个设置。 我发现添加和删除 ObservableCollection 绑定到 ListView 的项目的顺序很重要。如果我先添加新项目然后删除旧项目,则会重复列表项目。如果我先删除旧项目然后添加新项目,则这些项目不会重复。但是,我使用AutoMapper.Collection 来更新这个集合,所以我无法控制顺序。 一位同事建议这个bug可能与ListView.SelectedItem有关。我发现,如果我在将所选项目从集合中删除之前将其设置为 null,则列表项不会重复。这是我现在使用的解决方案。

这是一个例子:

    // This resolves the issue:
    if (!widgetsToLoad.Contains(SelectedWidget))
    
        SelectedWidget = null;
    

    // AutoMapper.Collection updates collections in this order. The issue does not occur
    // if the order of these two lines of code is reversed.
    
        // Add widgets in this group
        widgetsToLoad.Except(Widgets).ToList().ForEach(w => Widgets.Add(w));
        // Remove widgets not in this group
        Widgets.Except(widgetsToLoad).ToList().ForEach(w => Widgets.Remove(w));
    

如需完整重现,请将问题中的代码块替换为以下更改:

MainPage.xaml

<Page
    x:Class="ListViewDuplicateItem_Fallback.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ListViewDuplicateItem_Fallback">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <ComboBox
            Grid.Row="0"
            Grid.Column="0"
            ItemsSource="x:Bind ViewModel.Groups"
            SelectedItem="x:Bind ViewModel.SelectedGroup, Mode=TwoWay" />
        <ListView
            Grid.Row="1"
            Grid.Column="0"
            ItemsSource="x:Bind ViewModel.Widgets"
            SelectedItem="x:Bind ViewModel.SelectedWidget, Mode=TwoWay">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:Widget">
                    <TextBlock Text="x:Bind Id" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <TextBox
            Grid.Row="1"
            Grid.Column="1"
            Text="x:Bind ViewModel.SelectedWidget.Id, Mode=OneWay, FallbackValue=''" />
    </Grid>
</Page>

MainViewModel.cs

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;

namespace ListViewDuplicateItem_Fallback

    public class MainViewModel : INotifyPropertyChanged
    
        private string _selectedGroup;
        private Widget _selectedWidget;

        public MainViewModel()
        
            PropertyChanged += HomeViewModel_PropertyChanged;
            SelectedGroup = Groups.First();
        

        public event PropertyChangedEventHandler PropertyChanged;

        public ObservableCollection<string> Groups  get;  = new ObservableCollection<string>(DataSource.AllGroups);

        public string SelectedGroup
        
            get => _selectedGroup;
            set
            
                if (_selectedGroup != value)
                
                    _selectedGroup = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedGroup)));
                
            
        

        public Widget SelectedWidget
        
            get => _selectedWidget;
            set
            
                if (_selectedWidget != value)
                
                    _selectedWidget = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedWidget)));
                
            
        

        public ObservableCollection<Widget> Widgets  get;  = new ObservableCollection<Widget>();

        private void HomeViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
        
            if (e.PropertyName == nameof(SelectedGroup))
            
                var widgetsToLoad = DataSource.GetWidgetsForGroup(SelectedGroup);

                // This resolves the issue:
                //if (!widgetsToLoad.Contains(SelectedWidget))
                //
                //    SelectedWidget = null;
                //

                // AutoMapper.Collection updates collections in this order. The issue does not occur
                // if the order of these two lines of code is reversed. I do not simply clear the
                // collection and reload it because this clears the selected item even when it is in
                // both groups, and the animation is much smoother if items are not removed and reloaded.
                
                    // Add widgets in this group
                    widgetsToLoad.Except(Widgets).ToList().ForEach(w => Widgets.Add(w));
                    // Remove widgets not in this group
                    Widgets.Except(widgetsToLoad).ToList().ForEach(w => Widgets.Remove(w));
                

                // Select the first widget
                if (SelectedWidget == null && Widgets.Any())
                
                    SelectedWidget = Widgets.First();
                
            
        
    

DataSource.cs

using System.Collections.Generic;
using System.Linq;

namespace ListViewDuplicateItem_Fallback

    public static class DataSource
    
        public static List<string> AllGroups  get; set;  = new List<string>  "Group 1", "Group 2", "Group 3" ;

        public static List<Widget> AllWidgets  get; set;  = new List<Widget>(Enumerable.Range(1, 11).Select(widgetId => new Widget  Id = widgetId ));

        public static List<Widget> GetWidgetsForGroup(string group)
        
            switch (group)
            
                case "Group 1":
                    return AllWidgets.Take(4).ToList();

                case "Group 2":
                    return AllWidgets.Skip(4).Take(4).ToList();

                case "Group 3":
                    return AllWidgets.Take(1).Union(AllWidgets.Skip(8).Take(3)).ToList();

                default:
                    return new List<Widget>();
            
        
    

【讨论】:

以上是关于我的 ListView 两次显示相同的项目。我如何解决它?的主要内容,如果未能解决你的问题,请参考以下文章

绘图图例显示未知项目/相同的图例项目以不同的线条显示两次

如何以不同的条件选择相同的字段两次并将结果显示为单独的字段

带有自定义 CellFactory 的 ListView 修剪不可见节点

QMLProfiler 中的 QML Listview 报告两次委托创建

如何使 ListView 中的项目打开一个新的空活动

如何在opencart的同一页面上两次使用相同脚本中的脚本?