Мой ListView показывает один и тот же элемент дважды. Как мне это исправить? - PullRequest
0 голосов
/ 16 марта 2020

У меня есть 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); }
        }
    }
}

Ответы [ 2 ]

0 голосов
/ 28 марта 2020

Обновление моего проекта для использования {x: Bind} (скомпилированные привязки) появилось, чтобы решить проблему, но неделю спустя я неожиданно начал снова видеть дубликаты в моем ListView. На этот раз я обнаружил еще три фактора, которые привели к этой проблеме.

  1. Я добавил FallbackValue к TextBoxes, привязанному к SelectedItem, чтобы они были очищены, когда ни один элемент не был выбран. Если я удаляю FallbackValue, элементы списка не дублируются. Однако мне нужен этот параметр.
  2. Я обнаружил, что порядок, в котором я добавляю и удаляю элементы с ObservableCollection, привязанным к ListView, важен. Если я сначала добавляю новые элементы, а затем удаляю старые, элементы списка дублируются. Если я сначала удаляю старые элементы, а затем добавляю новые, они не дублируются. Однако я использую AutoMapper.Collection для обновления этой коллекции, поэтому я не могу контролировать порядок.
  3. Коллега предположил, что эта ошибка может быть связана с 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>();
            }
        }
    }
}
0 голосов
/ 16 марта 2020

Кажется, что это происходит только тогда, когда проект включает пользовательский элемент управления, который использует разметку Binding.

В приведенном выше примере, если MyControl удаляется из MainPage.xaml, он работает, как и ожидалось.

Аналогично, если <local:MyControl Text="{Binding ViewModel.SelectedWidget.Id}" /> изменяется на <local:MyControl Text="{x:Bind ViewModel.SelectedWidget.Id}" />, пример работает как ожидалось

Это похоже на ошибку в элементе управления ListView, но вы можете обойти ее, используя {x: Bind} скомпилированные привязки .

Редактировать: При дальнейшем исследовании пользовательским элементом управления могла быть красная сельдь. Изменение пользовательского элемента управления на стандартный TextBox не решает проблему, как я думал ранее. Проблема может быть воспроизведена без пользовательского контроля. Тем не менее, использование {x: Bind} или полное удаление элемента управления решает проблему в этом случае.

...