我的场景:我有一个后台线程轮询更改并定期更新WPF DataGrid的ObservableCollection(MVVM样式)。用户可以单击DataGrid中的行,并在同一主视图上的相邻UserControl中显示该行的“详细信息”。
当后台线程有更新时,它循环遍历ObservableCollection中的对象并替换单个对象(如果它们已更改)(换句话说,我不是将整个新的ObservableCollection重新绑定到DataGrid,而是替换单个项目中的单个项目)集合;这允许DataGrid在更新期间维护排序顺序)。
问题是,在用户选择了特定行并且细节显示在相邻的UserControl中后,当后台线程更新DataGrid时,DataGrid会丢失SelectedItem(它会重置为-1的索引)。
如何在ObservableCollection的更新之间保留SelectedItem?
答案 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,但我会尝试这种方法:
向视图模型添加一个属性,该属性将保存当前所选项的值。
使用SelectedItem
将TwoWay
绑定到此新媒体资源。
这样,当用户选择一行时,它将更新视图模型,当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();
这样,如果购物车无效,我可以将网格中的行颜色更改为红色,而且可以保留我的选择。
编辑:拼写