Прокрутите WPF DataGrid, чтобы отобразить выбранный элемент сверху.

У меня есть DataGrid со многими элементами, и мне нужно программно прокрутить до SelectedItem. Я искал StackOverflow и Google, и кажется, что решение - ScrollIntoView, как показано ниже:

grid.ScrollIntoView(grid.SelectedItem)

который прокручивает DataGrid вверх или вниз, пока выбранный элемент не будет в фокусе. Однако, в зависимости от текущей позиции прокрутки относительно выбранного элемента, выбранный элемент может стать последним видимым элементом в DataGrid ScrollViewer. Я хочу, чтобы выбранный элемент был первым видимым элементом в ScrollViewer (предполагая, что в DataGrid достаточно строк). Поэтому я попробовал это:

'FindVisualChild is a custom extension method that searches in the visual tree and returns 
'the first element of the specified type
Dim sv = grid.FindVisualChild(Of ScrollViewer)
If sv IsNot Nothing Then sv.ScrollToEnd()
grid.ScrollIntoView(grid.SelectedItem)

Сначала я прокручиваю до конца DataGrid и только затем прокручиваю его до SelectedItem, после чего SelectedItem отображается в верхней части DataGrid.

Моя проблема в том, что прокрутка до конца DataGrid работает хорошо, но последующая прокрутка к выбранному элементу не всегда работает.

Как я могу решить эту проблему или есть ли другая альтернативная стратегия прокрутки определенной записи в верхней позиции?

Ответ 1

Вы были на правильном пути, просто попробуйте работать с представлением коллекции вместо того, чтобы работать непосредственно с datagrid для таких потребностей.

Вот рабочий пример, где желаемый элемент всегда отображается как первый выбранный элемент, если это возможно, иначе прокрутка прокрутки до конца и целевого элемента выбирается в его положении.

Ключевыми моментами являются:

  • Используйте CollectionView на стороне бизнеса и включите синхронизацию текущего элемента в элементе управления XAML (IsSynchronizedWithCurrentItem=true)
  • Отложите "реальную" целевую прокрутку, чтобы разрешить визуализацию "Выбрать последний элемент" (используя Dispatcher.BeginInvoke с низким приоритетом)

Вот бизнес-логика (это автоматическое преобразование из С# в VB)

Public Class Foo

    Public Property FooNumber As Integer
        Get
        End Get
        Set
        End Set
    End Property
End Class

Public Class MainWindow
    Inherits Window
    Implements INotifyPropertyChanged

    Private _myCollectionView As ICollectionView

    Public Sub New()
        MyBase.New
        DataContext = Me
        InitializeComponent
        MyCollection = New ObservableCollection(Of Foo)
        MyCollectionView = CollectionViewSource.GetDefaultView(MyCollection)
        Dim i As Integer = 0
        Do While (i < 50)
            MyCollection.Add(New Foo)
            i = (i + 1)
        Loop

    End Sub

    Public Property MyCollectionView As ICollectionView
        Get
            Return Me._myCollectionView
        End Get
        Set
            Me._myCollectionView = value
            Me.OnPropertyChanged("MyCollectionView")
        End Set
    End Property

    Private Property MyCollection As ObservableCollection(Of Foo)
        Get
        End Get
        Set
        End Set
    End Property

    Private Sub ButtonBase_OnClick(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Dim targetNum As Integer = Convert.ToInt32(targetScroll.Text)
        Dim targetObj As Foo = Me.MyCollection.FirstOrDefault(() => {  }, (r.FooNumber = targetNum))

        'THIS IS WHERE THE MAGIC HAPPENS
        If (Not (targetObj) Is Nothing) Then
            'Move to the collection view to the last item
            Me.MyCollectionView.MoveCurrentToLast
            'Bring this last item into the view
            Dim current = Me.MyCollectionView.CurrentItem
            itemsContainer.ScrollIntoView(current)
            'This is the trick : Invoking the real target item select with a low priority allows previous visual change (scroll to the last item) to be executed
            Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, New Action(() => {  }, Me.ScrollToTarget(targetObj)))
        End If

    End Sub

    Private Sub ScrollToTarget(ByVal targetObj As Foo)
        Me.MyCollectionView.MoveCurrentTo(targetObj)
        itemsContainer.ScrollIntoView(targetObj)
    End Sub

    Public Event PropertyChanged As PropertyChangedEventHandler

    Protected Overridable Sub OnPropertyChanged(ByVal propertyName As String)
        If (Not (PropertyChanged) Is Nothing) Then
            PropertyChanged?.Invoke(Me, New PropertyChangedEventArgs(propertyName))
        End If

    End Sub
End Class

И это xaml

 <Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <DataGrid x:Name="itemsContainer" ItemsSource="{Binding MyCollectionView}" IsSynchronizedWithCurrentItem="True"  Margin="2" AutoGenerateColumns="False" >
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding FooNumber}"></DataGridTextColumn>
        </DataGrid.Columns>
    </DataGrid>

    <StackPanel Grid.Column="1">
        <TextBox x:Name="targetScroll" Text="2" Margin="2"></TextBox>
        <Button Content="Scroll To item" Click="ButtonBase_OnClick" Margin="2"></Button>
    </StackPanel>
</Grid>

Ответ 2

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

Ответ 3

Я решил этот вопрос следующим кодом:

public partial class MainWindow:Window
{
    private ObservableCollection<Product> products=new ObservableCollection<Product> ();

    public MainWindow()
    {
        InitializeComponent ();

        for (int i = 0;i < 50;i++)
        {
            Product p=new Product { Name="Product "+i.ToString () };
            products.Add (p);
        }

        lstProduct.ItemsSource=products;
    }

    private void lstProduct_SelectionChanged(object sender,SelectionChangedEventArgs e)
    {
        products.Move (lstProduct.SelectedIndex,0);
        lstProduct.ScrollIntoView (lstProduct.SelectedItem);
    }
}

public class Product
{
    public string Name { get; set; }
}


<Grid>
    <ListBox Name="lstProduct" Margin="20" DisplayMemberPath="Name" SelectionChanged="lstProduct_SelectionChanged" />
</Grid>