Search code examples
c#wpfmvvmdatagrid

Handling selection change in view-model for DataGrid.ScrollIntoView


I have the following view-model class:

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;
}

and the following view:

<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>

I set the ItemsSource of the DataGrid in the code-behind as follows:

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

Selecting/deselecting rows in the data grid is propogated to the view-model instances, and changes from code to the view-model's IsSelected property are propogated back to the data grid.

But I want that when the IsSelected property is set via code within the view model:

data[79].IsSelected = true;

the selected data grid row should also scroll into view, presumably using the data grid's ScrollIntoView method.


My original thought was to listen in the view code-behind for the SelectionChanged event:

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

But this doesn't work, as SelectionChanged is only triggered on visible items when virtualization is on.

Turning virtualization off is a successful workaround:

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

but I'm worried about the performance implications for large lists (20K+ items), so I would prefer not to do this.


What's the MVVM way of doing this?


Solution

  • You may not like referencing View controls in your ViewModels, but this will work.

    Create a reference to the DataGrid in your ViewModel and invoke the ScrollIntoView command in the Data setter.

    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; }
    }
    

    Then just add the reference when you construct the ViewModels

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

    It seems like the best way to avoid virtualization and having to call from the View in this case.