【问题标题】:My ListView is showing the same item twice. How do I fix it?我的 ListView 两次显示相同的项目。我如何解决它?
【发布时间】: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); }
        }
    }
}

【问题讨论】:

    标签: c# xaml listview data-binding uwp


    【解决方案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 中再次看到重复的项目。这次我发现了导致此问题的其他三个因素。

      1. 我在绑定到SelectedItemTextBoxes 中添加了FallbackValue,以便在未选择任何项目时将其清除。如果我删除 FallbackValue,列表项不会重复。不过,我需要这个设置。
      2. 我发现添加和删除 ObservableCollection 绑定到 ListView 的项目的顺序很重要。如果我先添加新项目然后删除旧项目,则会重复列表项目。如果我先删除旧项目然后添加新项目,则这些项目不会重复。但是,我使用AutoMapper.Collection 来更新这个集合,所以我无法控制顺序。
      3. 一位同事建议这个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>();
                  }
              }
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-10-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-02-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多