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

9
我的情景是这样的:我有一个后台线程,它轮询变化并定期更新 WPF DataGrid 的 ObservableCollection(MVVM 样式)。用户可以在 DataGrid 中点击一行,并在同一主视图上的相邻 UserControl 中打开该行的“详细信息”。
当后台线程有更新时,它会循环遍历 ObservableCollection 中的对象,并替换已更改的单个对象(换句话说,我不是将整个新 ObservableCollection 重新绑定到 DataGrid,而是替换集合中的单个项目;这使得 DataGrid 在更新期间保持排序顺序)。
问题是,当用户选择特定行并在相邻的 UserControl 中显示详细信息时,当后台线程更新 DataGrid 时,DataGrid 就会失去 SelectedItem(它被重置回索引 -1)。
如何在更新 ObservableCollection 时保留 SelectedItem?
4个回答

7
如果您的网格是单选的,我的建议是使用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);
            }
        }
    }
}

OLD SAMPLE:

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,这使得它成为一个稍微不同的问题。我不能只在按钮单击处理程序中处理所有内容。 - Chris Holmes
我并不是建议你将所有东西都处理在一个按钮的点击事件里。那只是一个示例代码。思想是,你在ViewModel中做一些类似于我示例中Button_Click事件中所做的事情。让我尝试给你写一个MVVM示例。我会尽快回复你。 - ASanch
请看编辑。希望这能让您更好地理解。它包含一个ListBox,其数据每5秒钟随机更新一次。您会发现它在更新后仍然保留了选择。 - ASanch

3
我没有使用过WPF DataGrid,但我建议你尝试以下方法:
在视图模型中添加一个属性,用于保存当前选定项的值。
SelectedItem绑定到这个新属性,使用TwoWay
这样,当用户选择一行时,它会更新视图模型,当ObservableCollection被更新时,它不会影响SelectedItem绑定的属性。因为被绑定,我不认为它会以你所看到的方式重置。

那是我最初的想法,Jay,但到目前为止还没有进展。它仍然在重置中。 - Chris Holmes
@Chris 选定的项目本身是否正在更新,还是只有其他项目在更新? - Jay

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

0

这个问题可能已经解决了,但是这里有一个我所做的例子,它适用于购物车网格。 我有一个带有ObservableCollection和CollectionView的数据网格,从包含购物车的本地变量中填充:

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

稍后我会在一个函数中更改购物车的“Valid”属性 - 不是直接更改,但重要的是ObservableCollection中的项发生了变化。为了反映这个变化并保持选择,我只需要刷新CollectionViewSource(注意内部View):
        var cart = _formCarts.ElementAt(index-1);
        cart.Valid = validity;
        _cartsViewSource.View.Refresh();

这样我就能够在网格中将行颜色更改为红色,如果购物车无效,同时保留我的选择。

编辑:拼写错误


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