处理DataGrid视图模型中的选择更改.滚动到视图



我有以下视图模型类:

public class ViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
private bool _isSelected;
public bool IsSelected {
get => _isSelected;
set {
if (value == _isSelected) { return; }
_isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
}
}
public int Data { get; }
public ViewModel(int data) => Data = data;
}

和以下视图:

<Window x:Class="MVVMScrollIntoView.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DataGrid Name="dg">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Window>

我在后面的代码中设置了DataGrid的ItemsSource,如下所示:

var data = Enumerable.Range(1, 100).Select(x => new ViewModel(x)).ToList();
dg.ItemsSource = data;

在数据网格中选择/取消选择行将按比例分配给视图模型实例,并且从代码到视图模型的IsSelected属性的更改将按比例返回到数据网格。

但我希望当IsSelected属性通过视图模型中的代码设置时:

data[79].IsSelected = true;

所选的数据网格行也应该滚动到视图中,大概使用数据网格的ScrollIntoView方法。


我最初的想法是在SelectionChanged事件的视图代码后面侦听:

dg.SelectionChanged += (s, e) => dg.ScrollIntoView(dg.SelectedItem);

但这不起作用,因为SelectionChanged只在虚拟化打开时在可见项目上触发

关闭虚拟化是一个成功的解决方法:

<DataGrid Name="dg" EnableRowVirtualization="False">
...

但我担心大列表(20K+项(的性能影响,所以我不想这样做。


MVVM的方法是什么?

既然我被邀请发布答案,就在这里。
根据需求,MVVM有几种方法可以做到这一点(仅限单一/扩展选择或取消选择等(。
既然你想使用虚拟化,那么

  • 您必须通知DataGrid,该选择已更改
  • 您必须使用ScrollIntoView对选择更改做出反应

要实现第一点,必须进行

  • CurrentlySelected属性粘贴到"父"-ViewModel
  • 将其与DataGrid.SelectedItem绑定
  • 设置CurrentlySelected如果IsSelected正在更改,也有几种方法,但例如,您可以将父ViewModel的引用传递给子ViewModel

要实现第二点,您可以例如使用DataGrid.SelectionChanged的事件处理程序,但我更愿意将此功能应用到行为中,因为它不在代码背后,您可以重用它。

父视图模型:

ViewModel _currentlySelected;
public ViewModel CurrentlySelected
{
get
{
return _currentlySelected;
}
set
{
if (_currentlySelected != value)
{
_currentlySelected = value;
NotifyPropertyChanged(nameof(CurrentlySelected));
}
}
}

ViewModel:

public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool _isSelected;
public ParentVM ParentRef { get; set; }
public bool IsSelected
{
get => _isSelected;
set
{
if (value == _isSelected) { return; }
_isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
if (ParentRef != null && _isSelected)
{
ParentRef.CurrentlySelected = this;
}
}
}
public int Data { get; }
public ViewModel(int data) => Data = data;
}

初始化:

var data = Enumerable.Range(1, 100).Select(x => new ViewModel(x){ParentRef=(this.DataContext as ParentVM)}).ToList();
dg.ItemsSource = data;

行为:

using System.Windows.Controls;
using System.Windows.Interactivity;
public class ScrollSelectedIntoView : Behavior<DataGrid>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
}
private void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
AssociatedObject?.ScrollIntoView(AssociatedObject?.SelectedItem);
}
protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
base.OnDetaching();
}
}

XAML:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
<DataGrid x:Name="dg" EnableRowVirtualization="True"
SelectedItem="{Binding CurrentlySelected}">
<i:Interaction.Behaviors>
<local:ScrollSelectedIntoView/>
</i:Interaction.Behaviors>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</DataGrid.RowStyle>
</DataGrid>

您可能不喜欢在ViewModels中引用View控件,但这是可行的。

ViewModel中创建对DataGrid的引用,并在Datasetter中调用ScrollIntoView命令。

public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
if (value == _isSelected) { return; }
_isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
//Invoke Scroll
DataGrid.ScrollIntoView(this);
}
}
public int Data { get; }
public ViewModel(int data) => Data = data;
//DataGrid Reference
public DataGrid DataGrid { get; set; }
}

然后只需在构建ViewModels 时添加引用

var data = Enumerable.Range(1, 100).Select(x => new ViewModel(x) { DataGrid = dg }).ToList();

在这种情况下,这似乎是避免虚拟化和必须从视图调用的最佳方法。

我会创建一个ParentViewModel,并将一个List与另一个引用SelectedViewModel的属性合并,然后订阅ViewModel的每个PropertyChanged事件。当ParentViewModel收到ViewModel的IsSelected属性已更改的通知时,它会将其SelectedViewModel设置为该发件人。然后在视图中,订阅ParentViewModel的PropertyChanged并检查SelectedViewModel何时更新,然后将数据网格缩放到该项。

代码隐藏

namespace WpfApplication2
{
public class ViewModel : INotifyPropertyChanged
{
private bool _isSelected;
public ViewModel(int data)
{
Data = data;
}
public int Data { get; }
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value == _isSelected) return;
_isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class ParentViewModel : INotifyPropertyChanged
{
private ViewModel _selectedViewModel;
public ParentViewModel(List<ViewModel> viewModels)
{
ViewModels = viewModels;
foreach (var vm in viewModels)
{
vm.PropertyChanged += (sender, args) =>
{
if (args.PropertyName != nameof(ViewModel.IsSelected)) return;
SelectedViewModel = vm;
};
}
}
public ViewModel SelectedViewModel
{
get { return _selectedViewModel; }
set
{
if (value == _selectedViewModel) return;
_selectedViewModel = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedViewModel)));
}
}
public List<ViewModel> ViewModels { get; }
public event PropertyChangedEventHandler PropertyChanged;
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private ParentViewModel _parentViewModel;
public MainWindow()
{
InitializeComponent();
var parentViewModel = new ParentViewModel(Enumerable.Range(1, 100).Select(x => new ViewModel(x)).ToList());
_parentViewModel = parentViewModel;
_parentViewModel.PropertyChanged += (sender, args) =>
{
if (args.PropertyName != nameof(ParentViewModel.SelectedViewModel)) return;
var selectedViewModel = _parentViewModel.SelectedViewModel;
if (selectedViewModel != null && selectedViewModel.IsSelected)
{
this.dataGrid.ScrollIntoView(selectedViewModel);
}
};
dataGrid.ItemsSource = _parentViewModel.ViewModels;
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
_parentViewModel.ViewModels[66].IsSelected = true;
}
}
}

XAML

<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication2"
mc:Ignorable="d"
Title="MainWindow"
Height="350"
Width="525">
<StackPanel Orientation="Horizontal">
<DataGrid Name="dataGrid"
EnableColumnVirtualization="True"
EnableRowVirtualization="True">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="IsSelected"
Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Style>
</DataGrid.RowStyle>
</DataGrid>
<Button Click="ButtonBase_OnClick"
Content="Click" />
</StackPanel>
</Window>

最新更新