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

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

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

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

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

【问题讨论】:

    标签: c# wpf multithreading datagrid observablecollection


    【解决方案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();
      

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

      编辑:拼写

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-10-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-02-15
        • 2013-04-19
        • 1970-01-01
        相关资源
        最近更新 更多