我的 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
,它会按预期工作。
同样,如果将<local:MyControl Text="Binding ViewModel.SelectedWidget.Id" />
更改为<local:MyControl Text="x:Bind ViewModel.SelectedWidget.Id" />
,该示例将按预期工作
这似乎是ListView
控件中的一个错误,但您可以使用x:Bind compiled bindings 解决它。
编辑:经过进一步调查,自定义控件可能是红鲱鱼。将自定义控件更改为标准 TextBox 并不能解决我之前认为的问题。该问题可以在没有自定义控件的情况下重现。尽管如此,使用 x:Bind 或完全删除控件确实可以解决这种情况下的问题。
【讨论】:
【参考方案2】:更新我的项目以使用x:Bind (compiled bindings) 似乎解决了这个问题,但一周后我意外地开始在我的ListView
中再次看到重复的项目。这次我发现了导致此问题的其他三个因素。
-
我在绑定到
SelectedItem
的TextBoxes
中添加了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 修剪不可见节点