如何防止 WPF DataGrid 在项目更新时取消选择 SelectedItem?

Posted

技术标签:

【中文标题】如何防止 WPF DataGrid 在项目更新时取消选择 SelectedItem?【英文标题】:How To Prevent WPF DataGrid From De-Selecting SelectedItem When Items Updated? 【发布时间】:2011-04-07 10:10:38 【问题描述】:

我的场景:我有一个后台线程来轮询更改并定期更新 WPF DataGrid 的 ObservableCollection(MVVM 样式)。用户可以单击 DataGrid 中的一行,并在同一主视图上的相邻 UserControl 中显示该行的“详细信息”。

当后台线程有更新时,它会循环遍历 ObservableCollection 中的对象,并在个别对象发生变化时替换它们(换句话说,我不是将一个全新的 ObservableCollection 重新绑定到 DataGrid,而是替换集合;这允许 DataGrid 在更新期间保持排序顺序)。

问题在于,在用户选择了特定行并且详细信息显示在相邻的 UserControl 中之后,当后台线程更新 DataGrid 时,DataGrid 会丢失 SelectedItem(它被重置为索引 -1)。

如何在 ObservableCollection 更新之间保留 SelectedItem?

【问题讨论】:

【参考方案1】:

如果您的网格是单选的,我的建议是您使用 CollectionView 作为 ItemsSource 而不是实际的 ObservableCollection。然后,确保 Datagrid.IsSynchronizedWithCurrentItem 设置为 true。最后,在“替换项目逻辑”的最后,只需将 CollectionView 的 CurrentItem 移动到相应的新项目。

下面是一个示例来演示这一点。 (不过,我在这里使用 ListBox。希望它适用于您的 Datagrid)。

编辑 - 使用 MVVM 的新示例:

XAML

<Window x:Class="ContextTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Name="window"
        Title="MainWindow" Height="350" Width="525">
    <DockPanel>
        <ListBox x:Name="lb" DockPanel.Dock="Left" Width="200" 
                 ItemsSource="Binding ModelCollectionView"
                 SelectionMode="Single" IsSynchronizedWithCurrentItem="True">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="Binding Path=Name"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <TextBlock Text="Binding ElementName=lb, Path=SelectedItem.Description"/>

    </DockPanel>
</Window>

代码隐藏:

using System;
using System.Windows;
using System.Windows.Data;
using System.Collections.ObjectModel;
using System.Windows.Threading;

namespace ContextTest

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    
        public MainWindow()
        
            InitializeComponent();
            this.DataContext = new ViewModel();
        
    

    public class ViewModel
    
        private DataGenerator dataGenerator;
        private ObservableCollection<Model> modelCollection;
        public ListCollectionView ModelCollectionView  get; private set; 

        public ViewModel()
        
            modelCollection = new ObservableCollection<Model>();
            ModelCollectionView = new ListCollectionView(modelCollection);

            //Create models
            for (int i = 0; i < 20; i++)
                modelCollection.Add(new Model()  Name = "Model" + i.ToString(), 
                    Description = "Description for Model" + i.ToString() );

            this.dataGenerator = new DataGenerator(this);
        

        public void Replace(Model oldModel, Model newModel)
        
            int curIndex = ModelCollectionView.CurrentPosition;
            int n = modelCollection.IndexOf(oldModel);
            this.modelCollection[n] = newModel;
            ModelCollectionView.MoveCurrentToPosition(curIndex);
        
    

    public class Model
    
        public string Name  get; set; 
        public string Description  get; set; 
    

    public class DataGenerator
    
        private ViewModel vm;
        private DispatcherTimer timer;
        int ctr = 0;

        public DataGenerator(ViewModel vm)
        
            this.vm = vm;
            timer = new DispatcherTimer(TimeSpan.FromSeconds(5), 
                DispatcherPriority.Normal, OnTimerTick, Dispatcher.CurrentDispatcher);
        

        public void OnTimerTick(object sender, EventArgs e)
        
            Random r = new Random();

            //Update several Model items in the ViewModel
            int times = r.Next(vm.ModelCollectionView.Count - 1);
            for (int i = 0; i < times; i++)
               
                Model newModel = new Model() 
                     
                        Name = "NewModel" + ctr.ToString(),
                        Description = "Description for NewModel" + ctr.ToString()
                    ;
                ctr++;

                //Replace a random item in VM with a new one.
                int n = r.Next(times);
                vm.Replace(vm.ModelCollectionView.GetItemAt(n) as Model, newModel);
            
        
    

旧样本:

XAML:

<Window x:Class="ContextTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <ListBox x:Name="lb" SelectionMode="Single" IsSynchronizedWithCurrentItem="True" SelectionMode="Multiple">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="Binding Path=Name"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <TextBlock Text="Binding ElementName=lb, Path=SelectedItem.Name"/>
        <Button Click="Button_Click">Replace</Button>


    </StackPanel>
</Window>

代码隐藏:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace ContextTest

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    
        ObservableCollection<MyClass> items;
        ListCollectionView lcv;

        public MainWindow()
        
            InitializeComponent();

            items = new ObservableCollection<MyClass>();
            lcv = (ListCollectionView)CollectionViewSource.GetDefaultView(items);
            this.lb.ItemsSource = lcv;
            items.Add(new MyClass()  Name = "A" );
            items.Add(new MyClass()  Name = "B" );
            items.Add(new MyClass()  Name = "C" );
            items.Add(new MyClass()  Name = "D" );
            items.Add(new MyClass()  Name = "E" );

        

        public class MyClass
        
            public string Name  get; set; 
        

        int ctr = 0;
        private void Button_Click(object sender, RoutedEventArgs e)
        
            MyClass selectedItem = this.lb.SelectedItem as MyClass;
            int index = this.items.IndexOf(selectedItem);
            this.items[index] = new MyClass()  Name = "NewItem" + ctr++.ToString() ;
            lcv.MoveCurrentToPosition(index);
        

    

【讨论】:

对我来说真的不起作用 KarmicPuppet。问题是我正在做 MVVM,这使它成为一个稍微不同的问题。我不能只处理按钮单击处理程序中的所有内容。 我并不是建议您在按钮单击处理程序中处理所有事情。那只是一个示例代码。这个想法是您在 ViewModel 中执行与我的示例中的 Button_Click 事件类似的操作。让我试着给你写一个 MVVM 例子。我会回复你的。 见编辑。我希望这能给你一个更好的主意。它包含一个 ListBox,其数据每 5 秒随机更新一次。您会看到更新后它仍然保留选择。【参考方案2】:

我没有使用过 WPF DataGrid,但我会尝试这种方法:

向视图模型添加一个属性,该属性将保存当前选定项的值。

使用TwoWaySelectedItem 绑定到这个新属性。

这样,当用户选择一行时,它将更新视图模型,并且当ObservableCollection 更新时,它不会影响SelectedItem 绑定到的属性。受到约束,我不希望它会像你看到的那样重置。

【讨论】:

这是我首先想到的杰,但到目前为止还没有骰子。它仍在重置它。 @Chris 是所选项目本身正在更新,还是只是其他项目?【参考方案3】:

您可以在更新 Collection 的逻辑中,将 CollectionView.Current 项目引用保存到另一个变量。然后,完成更新后,调用 CollectionView.MoveCurrentTo(variable) 来重置所选项目。

【讨论】:

【参考方案4】:

它现在可能已经解决了,但这是我所做的一个示例,它适用于购物车网格。 我有一个带有 ObservableCollection 和 CollectionView 的数据网格,由包含购物车的局部变量填充:

        _cartsObservable = new ObservableCollection<FormOrderCart>(_formCarts);
        _cartsViewSource = new CollectionViewSource  Source = _cartsObservable ;
        CartsGrid.ItemsSource = _cartsViewSource.View;

稍后我在函数中更改了购物车的 Valid prop - 不是直接更改,但重要的是 ObservableCollection 中的项目发生了更改。为了反映更改和维护选择,我只需刷新 CollectionViewSource(注意内部视图):

        var cart = _formCarts.ElementAt(index-1);
        cart.Valid = validity;
        _cartsViewSource.View.Refresh();

这样,如果购物车无效,我可以将网格中的行颜色更改为红色,但也可以保留我的选择。

编辑:拼写

【讨论】:

以上是关于如何防止 WPF DataGrid 在项目更新时取消选择 SelectedItem?的主要内容,如果未能解决你的问题,请参考以下文章

如何防止 ComboBox 中的 NewItemPlaceholder 行绑定到与 WPF 中的 DataGrid 相同的 DataTable

c# wpf datagrid 模板列修改某个单元格,更新所选行另一个单元格的值,如何做到呢?

WPF DataGrid如何获取ItemsSource更新时

如何自动更新 WPF DataGrid 和 xml 之间的绑定

WPF DataGrid - 如何暂停数据绑定中的 UI 更新并稍后进行批量更新

如何根据 WPF DataGrid 中的更改更新 ObservableCollection 项属性?