DataGrid选择问题

3
我遇到的问题是这样的:当选中DataGrid的前两行并删除第一行时,下面的选择行变成第一行并且取消了选择。如果我对任何一个列进行排序,那么该行的选择会恢复。或者,如果我关闭窗口,会收到信息提示取消选择的行实际上是被选中的(在UsersViewModel的SelectedUsers属性中查询底层绑定属性的内容)。有没有人能够帮忙解释一下我是否做错了什么,或者这可能是个bug。我在下面提供完整的源代码。感谢您的帮助。
MainWindow.xaml
<Window x:Class="DeleteFirstRowIssue.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DeleteFirstRowIssue"
        Title="MainWindow" Height="350" Width="400">
    <Window.Resources>
        <Style x:Key="CustomDataGridCellStyle" TargetType="{x:Type DataGridCell}">
                <Style.Triggers>
                    <Trigger Property="IsSelected" Value="True">
                        <Setter Property="Background" Value="Red"/>
                        <Setter Property="FontWeight" Value="Bold"/>
                        <Setter Property="Foreground" Value="Black"/>
                    </Trigger>
                </Style.Triggers>
            </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="25"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Label Content="Users:" Grid.Row="0" Grid.Column="0"/>
        <local:CustomDataGrid x:Name="UsersDataGrid" ItemsSource="{Binding UsersViewSource.View}" SelectionMode="Extended" AlternatingRowBackground="LightBlue" AlternationCount="2"
                              SelectionUnit="FullRow" IsReadOnly="True" SnapsToDevicePixels="True" AutoGenerateColumns="False" Grid.Row="1" Grid.Column="0" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False"
                              SelectedItemsList="{Binding SelectedUsers, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsSynchronizedWithCurrentItem="False" CellStyle="{StaticResource CustomDataGridCellStyle}">
            <local:CustomDataGrid.Columns>
                <DataGridTextColumn Header="Nickname:" Width="*" Binding="{Binding Nickname}"/>
                <DataGridTextColumn Header="Age:" Width="*" Binding="{Binding Age}"/>
            </local:CustomDataGrid.Columns>
        </local:CustomDataGrid>
        <Button Grid.Row="2" Grid.Column="0" Margin="5" MaxWidth="80" MinWidth="80" Content="Delete 1st row" Command="{Binding DeleteFirstUserCommand}"/>
        <Button Grid.Row="3" Grid.Column="0" Margin="5" MaxWidth="80" MinWidth="80" Content="Delete last row" Command="{Binding DeleteLastUserCommand}"/>
        <Button Grid.Row="4" Grid.Column="0" Margin="5" MaxWidth="80" MinWidth="80" Content="Initialize Grid" Command="{Binding InitializeListCommand}"/>
    </Grid>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;

namespace DeleteFirstRowIssue
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new UsersViewModel();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            UsersViewModel uvm = (UsersViewModel)DataContext;
            if (uvm.SelectedUsers.Count > 0)
            {
                StringBuilder sb = new StringBuilder(uvm.SelectedUsers.Count.ToString() + " selected user(s):\n");
                foreach (UserModel um in uvm.SelectedUsers)
                {
                    sb.Append(um.Nickname + "\n");
                }
                MessageBox.Show(sb.ToString());
            }
            base.OnClosing(e);
        }
    }

    public class UsersViewModel : INotifyPropertyChanged
    {
        private IList selectedUsers;
        public IList SelectedUsers
        {
            get { return selectedUsers; }
            set
            {
                selectedUsers = value;
                OnPropertyChanged("SelectedUsers");
            }
        }

        public CollectionViewSource UsersViewSource { get; private set; }
        public ObservableCollection<UserModel> Users { get; set; }
        public ICommand DeleteFirstUserCommand { get; }
        public ICommand DeleteLastUserCommand { get; }
        public ICommand InitializeListCommand { get; }
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }

        public UsersViewModel()
        {
            SelectedUsers = new ArrayList();
            Users = new ObservableCollection<UserModel>();
            UsersViewSource = new CollectionViewSource() { Source = Users };
            InitializeListCommand = new RelayCommand(p => Users.Count == 0, p => InitializeList());
            InitializeListCommand.Execute(null);
            DeleteFirstUserCommand = new RelayCommand(p => Users.Count > 0, p => DeleteFirstUser());
            DeleteLastUserCommand = new RelayCommand(p => Users.Count > 0, p => DeleteLastUser());
        }

        private void InitializeList()
        {
            Users.Add(new UserModel() { Nickname = "John", Age = 35 });
            Users.Add(new UserModel() { Nickname = "Jane", Age = 29 });
            Users.Add(new UserModel() { Nickname = "Mark", Age = 59 });
            Users.Add(new UserModel() { Nickname = "Susan", Age = 79 });
            Users.Add(new UserModel() { Nickname = "Joe", Age = 66 });
            Users.Add(new UserModel() { Nickname = "Nina", Age = 29 });
            Users.Add(new UserModel() { Nickname = "Selma", Age = 44 });
            Users.Add(new UserModel() { Nickname = "Katrin", Age = 24 });
            Users.Add(new UserModel() { Nickname = "Joel", Age = 32 });
        }

        private void DeleteFirstUser()
        {
            ListCollectionView lcw = (ListCollectionView)UsersViewSource.View;
            lcw.RemoveAt(0);
        }

        private void DeleteLastUser()
        {
            ListCollectionView lcw = (ListCollectionView)UsersViewSource.View;
            lcw.RemoveAt(lcw.Count - 1);
        }
    }

    public class UserModel
    {
        public string Nickname { get; set; }
        public int Age { get; set; }
    }

    public class CustomDataGrid : DataGrid
    {
        public static readonly DependencyProperty SelectedItemsListProperty = DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(CustomDataGrid), new PropertyMetadata(null));
        public IList SelectedItemsList
        {
            get { return (IList)GetValue(SelectedItemsListProperty); }
            set { SetValue(SelectedItemsListProperty, value); }
        }

        public CustomDataGrid() { SelectionChanged += CustomDataGrid_SelectionChanged; }
        void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) { SelectedItemsList = SelectedItems; }
    }

    public class RelayCommand : ICommand
    {
        private Predicate<object> canExecute;
        private Action<object> execute;

        public RelayCommand(Predicate<object> canExecute, Action<object> execute)
        {
            this.canExecute = canExecute;
            this.execute = execute;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter) { return canExecute(parameter); }
        public void Execute(object parameter) { execute(parameter); }
    }
}

有趣的是...如果您选择前3行,然后删除第1行,则以前的第3行(现在是第2行)将显示所选状态,但新的第1行不会。 - grek40
有趣的事实:刚刚用 Snoop 检查了一下,第一行中的 DataGridRow.IsSelectedDataGridCell.IsSelected 属性不同步。这就是为什么基于单元格的样式不再正确触发,但基于行的选择列表包含了这些项目。然而,我不知道为什么会出现这种详细的不同步情况。 - grek40
2个回答

1

正如评论所述,当删除第一行时,新的第一行IsSelected属性与其单元格IsSelected属性不同步。我不知道为什么会发生这种情况,但如果您主要关注保持样式工作,则表明可行的解决方法是:只需在触发器中使用行属性。

<DataTrigger Binding="{Binding IsSelected,RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}}" Value="True">
    <Setter Property="Background" Value="Red"/>
    <Setter Property="FontWeight" Value="Bold"/>
    <Setter Property="Foreground" Value="Black"/>
</DataTrigger>

非常感谢您的快速帮助。很好的解决方法。希望您不介意,如果我把功劳归于 Funk 解释了真正的原因。 - Cobek

1
我在实现SelectedItemsList时遇到了类似的问题。主要的陷阱是当选择被更新时,后台列表试图删除不再选择中的项目。然而,如果一个项目被更改(或不再存在),比较将失败,该项目将保留在列表中,导致这种行为。
您可以通过添加CollectionChanged事件处理程序来使列表保持同步。
public UsersViewModel()
{
    ...

    Users.CollectionChanged += new NotifyCollectionChangedEventHandler(CollectionChanged);
}

private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.Action == NotifyCollectionChangedAction.Remove)
        foreach (UserModel item in e.OldItems) SelectedUsers.Remove(item);
}

非常感谢您提供如此快速的解决方案和解释。 - Cobek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接