【问题标题】:DataGrid unable to deselected modified itemDataGrid 无法取消选择修改的项目
【发布时间】:2020-09-05 00:24:42
【问题描述】:

我有一个带有 DataGrid 的 WPF 应用程序。 此 DataGrid 绑定到包含一堆模型的 ObservableCollection。 此 DataGrid 的选择模式设置为 Extended。

如果当前选择了一个项目,然后收到更新(例如,在刷新期间所选项目的某些内容发生了变化),然后用户尝试选择另一个项目,它不会取消选择前一个项目。

OnselectionChanged 触发,但不包含先前的项目,似乎没有办法取消选择它。

运行此代码的代码(使用 .net core 3.1 清理 wpf 应用程序)

DataGrid,(如您所见)也不存储任何选定的项目。

            <DataGrid Grid.Row="1"
                  x:Name="ItemsDataGrid"
                  SelectionMode="Extended"
                  ItemsSource="{Binding Items}">
            <DataGrid.Resources>
                <Style TargetType="{x:Type DataGridCell}">
                    <Style.Triggers>
                        <Trigger Property="DataGridCell.IsSelected" Value="True">
                            <Setter Property="Background" Value="Red" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </DataGrid.Resources>
        </DataGrid>

项目,这是一个简单的 Observable 集合

private ObservableCollection<Window> _items = new ObservableCollection<Window>();

public ObservableCollection<Window> Items
{
    get => _items;
    set => SetProperty(ref _items, value);
 }

而改变项目的代码,它只是抓取第一个项目,并添加一个新的维度

 private void ChangeItem()
    {
        var rand = new Random();

        Items[0].Dimensions = new Rect(rand.Next(1, 100), rand.Next(1, 100), rand.Next(1, 100), rand.Next(1, 100));
    }

根据要求,这里是将项目添加到 ListBox 的代码

private void Button_Click(object sender, RoutedEventArgs e)
    {
        SelectedItemsListBox.Items.Clear();

        foreach(var item in ItemsDataGrid.SelectedItems)
        {
            SelectedItemsListBox.Items.Add(item);
        }

        SelectedItemsListBox.Items.Refresh();
    }

这里应用刚刚打开,点击“刷新”按钮显示3项

这里第一个项目已被选中,“更改项目”按钮已被单击以修改维度,还请注意该项目现在如何在下面显示它的描述,这是一个显示所有当前选定项目并被刷新的列表框(单击“显示选定项目”后,包括一个 listbox.clear)。

此处已单击第 3 个项目,将其选中,并且应该反过来取消选择先前选择的项目,但如列表框所示,它仍处于选中状态。

我已经将问题解决到这一点,它最终不是我的选择方法,不是我使用的 MultiSelector,我没有使用任何 UI 框架,这个准系统解决方案仍然存在让我陷入困境的问题说不出话来,不知道它可能是什么。

【问题讨论】:

  • DataGrid 似乎表现正常。显示太多项目的是ListBox。由于ListBox 也可以正常工作(除非您更改了它的行为),所以它必须是将所选项目添加到ItemsSource 的逻辑。有趣的是,特别是您知道不起作用的选择部分被完全忽略并且没有显示在您的帖子中。
  • 更新了帖子以包含列表框更新,话虽如此,你看这里是一个测试应用程序,我在实际应用程序中挣扎了 2 多天后创建。无论我如何填充列表框,选定的项目都保持选中状态,将项目绑定到属性也是如此,我存储了 MultiSelector 并检查了它的集合,它仍然存在并且在那里也很好。听了 OnSelectionChanged 也没有显示对旧项目的任何更改
  • 为什么叫SelectedItemsDataGrid,不就是ListBox吗?从您的图像中,只有一个项目被选中,因为只有一个红色行。这就是您处理收藏品的方式。
  • 又更新了代码,改了名字。它仍然被称为 DataGrid,因为我只是复制了一些 xaml 并在名称前面添加了选定的内容。它实际上指向 DataGrid 下方显示的列表框
  • 您能否建议我应该如何威胁收藏,正如我提到的那样。绑定 OnSelectionChanged docs.microsoft.com/en-us/dotnet/api/… 永远不会因为取消选择顶部项目而触发。使用 Multiselector 将其绑定到 ObservableCollection 也永远不会随着所述项目的移除而更新。循环遍历 DataGrid 本身的所有 SelectedItems(这是一个 100% 的 .net 项目,未经我的修改)仍然将该项目显示为选定项目。如上所示,无论视觉表示是什么

标签: c# .net wpf .net-core


【解决方案1】:

您观察到的行为与您的 GetHashCode 实现有关。

从您发布的类定义(在 Pastebin 上),我了解到您的数据项正在实现 IEquatable,并且在这种情况下还覆盖了 object.GetHashCode

您的实现根据可变字段计算哈希码!
通常应该避免这种情况,因为它可能导致意外行为(就像您现在正在经历的那样)。

“一般来说,对于可变引用类型,只有在以下情况下才应该覆盖 GetHashCode():

  • 您可以从不可变的字段计算哈希码;或
  • 您可以确保当对象包含在依赖于 它的哈希码。”

(微软文档:Notes to Inheritors

可变字段的问题在于,当对象用于基于散列的集合中时,它们可以更改
如果用于计算哈希码的字段发生变化,那么哈希码也会发生变化,从而导致原始哈希键的存储值丢失。

现在你要知道DataGridSelector一般都是用哈希表来存储选中的item来提高查找性能的。由于您的类型实现了IEquatable,因此DataGrid 倾向于使用GetHashCode 返回的值作为键,因为它假定了一个重写的实现。
GetHashCode 的这种实现在使用前由 DataGrid 检查可靠性,但显然这种可靠性检查没有考虑用于计算的字段的可变性。当然,这需要反思。避免反射并仅测试GetHashCode 在连续调用后保持不变的结果似乎很合理。

考虑到这一点,我们现在可以解释这种行为:

  • 选择一个项目并将其存储在选定项目哈希表中
  • DataGrid 使用项目本身作为基于散列的选定项目支持集合的键,以提高查找速度
  • 由于项目实现了IEquatable,哈希表很乐意在选定项目上调用GetHashCode,以获取对象哈希作为值(选定项目)的键
  • 您现在通过编辑 DataGrid 的单元格来修改项目,这也会导致计算的哈希码发生意外更改
  • 接下来选择一个不同的项目。 DataGrid 现在尝试从选定项目集合中删除先前选定的项目。但由于项目的哈希码已更改,查找不会返回任何项目。因此,旧的和取消选择的项目仍保留在选定项目集合中。

解决方案是遵循指南并避免覆盖object.GetHashCode,因为在您的情况下:

  • 无法从不可变(只读)的字段中计算哈希码并且
  • 不能确保当可变对象包含在依赖于其哈希码的集合中时,该对象的哈希码不会改变。

因此删除/修复 GetHashCode 覆盖将解决问题。


实施改进

添加一个ObservableCollection 类型的SelectedItems 集合作为ListBox 的绑定源并添加一个DataGrid.SelectionChanged 事件处理程序:

查看模型

private ObservableCollection<Window> _items = new ObservableCollection<Window>();
public ObservableCollection<Window> Items
{
    get => _items;
    set => SetProperty(ref _items, value);
 }

private ObservableCollection<Window> _selectedItems = new ObservableCollection<Window>();
public ObservableCollection<Window> SelectedItems
{
    get => _selectedItems ;
    set => SetProperty(ref _selectedItems, value);
}

查看(代码隐藏)

private void OnDataGridSelectionChanged(object sender, SelectionChangedEventArgs e)
{
  var viewModel = this.DataContext as ViewModel;
  foreach (Window addedItem in e.AddedItems.Cast<Window>())
  {
    viewModel.SelectedItems.Add(addedItem);
  }
  foreach (Window removedItem in e.RemovedItems.Cast<Window>())
  {
    viewModel.SelectedItems.Remove(removedItem);
  }
}

查看

<DataGrid SelectionMode="Extended"
          SelectionChanged="OnDataGridSelectionChanged">
   ...
</DataGrid>

<ListBox ItemsSource="{Binding SelectedItems}" />

【讨论】:

  • 感谢您提供样品。我完全按照此处所示的方式实现,除了我向列表框添加了显示成员路径,因此它实际上显示了项目描述。 hostr.co/file/FZxmqKoN4RDJ/Updatedwayofhandling.png 正如你在这张图片中看到的,它仍然显示为选定的项目,
  • 在我测试代码时你能做一个干净的重建并重试吗?
  • 是的,不是问题。如果你愿意,我什至可以上传我的测试项目本身,如果这是一种优先的方式。
  • 删除了 bin 和 obj 文件夹,清理并重建了项目,但恐怕仍然看到相同的结果。
  • 我已经使用 double 集合作为项目源测试了上述实现。它按预期工作。您能否删除其他所有内容,以便您的项目只包含两个控件。?除了在此属性上设置的数据绑定之外,任何代码都不应修改 DataGrid.ItemsSource。也可以将 Window 替换为 stringdouble 值。
【解决方案2】:

您在选择单元格和选择 DataGrid 的项目(行)之间感到困惑。 替换 DataGrid,我想你自己就会明白问题的原因:

    <DataGrid Grid.Row="1"
              x:Name="ItemsDataGrid"
              SelectionMode="Extended"
              ItemsSource="{Binding ItemsCollection}">
        <DataGrid.ItemContainerStyle>
            <Style TargetType="DataGridRow">
                <Style.Triggers>
                    <Trigger Property="IsSelected" Value="True">
                        <Setter Property="Background" Value="Red" />
                    </Trigger>
                </Style.Triggers>
            </Style>
        </DataGrid.ItemContainerStyle>
    </DataGrid>

如果@BionicCode 正确地编写了关于在集合项中实现 IEquatable 的内容,则将其删除。
虽然这不是问题的原因,但它可能会导致其他问题。
当集合中存在相等的项目时,ItemsControl(DataGrid 派生自它)不保证正确操作。
元素的相等性由 Equals() 和 GetHashCode() 方法确定。 这些方法也用于 DependecyObject 的内部实现中的许多其他方法。
因此,建议对用于 WPF 的数据使用引用类型,而不是更改默认的 Equals() 和 GetHashCode()。
如果需要按值进行比较,则必须使用不可变实现(可能是结构)或创建类似于 Equals() 和 GetHashCode() 的方法,但使用其他名称。

您还可以通过绑定到 SelectedItems 来补充 ListBox,它将实时显示所选内容:

    <ListBox Grid.Row="2" ItemsSource="{Binding SelectedItems, ElementName=ItemsDataGrid}"/>

【讨论】:

  • 这个答案没有帮助。请阅读我的回答以了解问题的本质。
  • 类型本身不必是不可变的。用于计算的字段必须是不可变的,例如,通过使用只读字段。
  • 也不建议从类型的签名中删除IEquatable。建议实现该接口。但是,如果您不能保证恒定的哈希码并且不能阻止在基于哈希的集合中使用该类型,则不应覆盖 GetHashCode。也不是关于ItemsControl。常见的类型是Selector,负责处理选中项。这种类型使用哈希表来存储选定的项目。
  • @BionicCode ,收藏项的实现我还没看。也许你是对的。
猜你喜欢
  • 1970-01-01
  • 2011-10-14
  • 2021-10-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-15
  • 2015-07-06
  • 1970-01-01
相关资源
最近更新 更多