Обработка изменения выбора в модели представления для DataGrid.ScrollIntoView - PullRequest
2 голосов
/ 07 ноября 2019

У меня есть следующий класс модели представления:

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>

Я установил ItemsSource DataGrid в коде позади какследует:

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

Выбор / отмена выбора строк в сетке данных распространяются на экземпляры модели представления, а изменения кода на свойство IsSelected модели представления переносятся обратно в сетку данных.

Но я хочу, чтобы, когда свойство IsSelected было установлено с помощью кода в модели представления:

data[79].IsSelected = true;

выбранная строка сетки данных также прокручивалась в представление, предположительно, с использованием сетки данных ScrollIntoView method.


Моей первоначальной мыслью было прослушивание в представлении кода для события SelectionChanged:

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

Но это не работает, так как SelectionChanged срабатывает только на видимых элементах, когда включена виртуализация.

Отключение виртуализации - это успешный обходной путь:

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

, но я беспокоюсь о влиянии на производительность для больших списков (20K +предметы), поэтому я бы предпочел не делать этогос.


Как MVVM это делает?

Ответы [ 3 ]

2 голосов
/ 11 ноября 2019

Поскольку меня приглашают опубликовать ответ, вот он.
Существует несколько способов сделать это с MVVM в зависимости от требований (выбор только для одного или расширенного выбора, а также отмена выбора и т. Д.).
Посколькувы хотите использовать виртуализацию, тогда

  • вы должны уведомить DataGrid, что выбор изменился
  • вы должны реагировать на изменение выбора с помощью ScrollIntoView

Для реализации первая точка необходимо

  • вставить в "родительский" -ViewModel свойство CurrentlySelected
  • связать егос DataGrid.SelectedItem
  • установить CurrentlySelected, если IsSelected изменяется, есть также несколько способов, но, например, вы можете передать ссылку на parent-ViewModel дочернему элементу.

Для реализации второй точки вы можете, например, использовать обработчик событий для DataGrid.SelectionChanged, но я бы предпочел применить эту функциональность к поведению, поскольку она не в коде позади, и вы можетеиспользовать его.

Родительская ViewModel:

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>
1 голос
/ 07 ноября 2019

Я бы создал ParentViewModel и выполнил гидрацию List с другим свойством, которое содержит ссылку на SelectedViewModel, а затем подписался бы на каждое из событий PropertyChanged ViewModel. Когда ParentViewModel получает уведомление о том, что свойство IsSelected ViewModel было изменено, он устанавливает свой SelectedViewModel этому отправителю. Затем в представлении вы подписываетесь на PropertyChanged объекта ParentViewModel и проверяете, когда 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>
1 голос
/ 07 ноября 2019

Вам может не нравиться ссылаться на элементы управления View в ваших ViewModels, но это будет работать.

Создайте ссылку на DataGrid в вашем ViewModel и вызовите команду ScrollIntoView в Datasetter.

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();

Кажется, что в этом случае лучший способ избежать виртуализации и необходимости вызывать из View.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...