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

时间:2010-09-02 15:45:08

标签: c# wpf multithreading datagrid observablecollection

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

当后台线程有更新时,它循环遍历ObservableCollection中的对象并替换单个对象(如果它们已更改)(换句话说,我不是将整个新的ObservableCollection重新绑定到DataGrid,而是替换单个项目中的单个项目)集合;这允许DataGrid在更新期间维护排序顺序)。

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

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

4 个答案:

答案 0 :(得分:6)

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

    }
}

答案 1 :(得分:3)

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

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

使用SelectedItemTwoWay绑定到此新媒体资源。

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

答案 2 :(得分:1)

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

答案 3 :(得分:0)

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

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

稍后我更改函数中的有效购物车属性-不是直接更改,但重要的是ObservableCollection中的项目有所更改。为了反映更改并保持选择,我只需刷新CollectionViewSource(注意内部视图):

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

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

编辑:拼写