How to prevent WPF DataGrid from changing due to selection of a selected item when updating items?

My scenario: I have a background thread that polls for changes and periodically updates the WSF DataGrid ObservableCollection (MVVM style). The user can click on a row in the DataGrid and display the "details" of this row in the adjacent UserControl on the same main screen.

When the background thread has updates, it cycles through the objects in the ObservableCollection and replaces individual objects if they have been changed (in other words, I do not interweave the whole new ObservableCollection in the DataGrid, but instead replace the individual elements in this, the DataGrid maintains the sort order during updates).

The problem is that after the user has selected a specific row and the details are displayed in the adjacent UserControl, when the background thread updates the DataGrid, the DataGrid loses the SelectedItem (it returns reset back to index -1).

How can I save SelectedItem between updates in an ObservableCollection?

+7
multithreading c # wpf datagrid observablecollection
source share
3 answers

If your grid is the only choice, I suggest you use CollectionView as the ItemsSource instead of the actual ObservableCollection. Then, make sure Datagrid.IsSynchronizedWithCurrentItem is set to true. Finally, at the end of your “element logic replacement”, simply move the CollectionView CurrentItem to the corresponding new element.

Below is a sample demonstrating this. (I use ListBox here. Hope it works fine with your Datagrid).

EDIT - A NEW SAMPLE FOR USING 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> 

Code-Behind:

 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> 

Code for:

 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); } } } 
+6
source share

I did not work with DataGrid WPF, but I would try this approach:

Add a property to the view model in which the value of the currently selected item will be held.

Bind SelectedItem to this new property using TwoWay .

Thus, when the user selects a row, he updates the view model, and when updating the ObservableCollection he will not affect the property to which the SelectedItem bound. Being connected, I would not expect it to be able to reset in the way you see.

+3
source share

In the logic that updates the collection, you can save a reference to the CollectionView.Current element to another variable. Then, after the update is complete, call CollectionView.MoveCurrentTo (variable) to reset the selected item.

+1
source share

All Articles