滚动WPF DataGrid以在顶部显示所选项目

时间:2023-01-19 10:09:02

I have a DataGrid with many items and I need to programmatically scroll to the SelectedItem. I have searched on * and Google, and it seems the solution is ScrollIntoView, as follows:

我有一个包含许多项的DataGrid,我需要以编程方式滚动到SelectedItem。我在*和Google上搜索过,似乎解决方案是ScrollIntoView,如下所示:

grid.ScrollIntoView(grid.SelectedItem)

which scrolls the DataGrid up or down until the selected item is in focus. However, depending on the current scroll position relative to the selected item, the selected item may end up being the last visible item in the DataGrid's ScrollViewer. I want that the selected item will be the first visible item in the ScrollViewer (assuming there are enough rows in the DataGrid to allow this). So I tried this:

向上或向下滚动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)

First I scroll to the end of the DataGrid and only then do I scroll to the SelectedItem, at which point the SelectedItem is shown at the top of the DataGrid.

首先,我滚动到DataGrid的末尾,然后才滚动到SelectedItem,此时SelectedItem显示在DataGrid的顶部。

My problem is that scrolling to the end of the DataGrid works well, but subsequently scrolling to the selected item doesn't always work.

我的问题是滚动到DataGrid的末尾运行良好,但随后滚动到所选项目并不总是有效。

How can I resolve this issue, or is there any other alternative strategy for scrolling to a specific record in the top position?

如何解决此问题,或者是否有其他替代策略可以滚动到顶部位置的特定记录?

3 个解决方案

#1


5  

You were on the right track, just try to work with collection view instead of working directly on the datagrid for this kind of needs.

您处于正确的轨道上,只是尝试使用集合视图,而不是直接在数据网格上工作以满足此类需求。

Here is a working example where the desired item is always displayed as first selected item if possible, otherwise the scrollviewer is scrolled to the end and the target item is selected at its position.

这是一个工作示例,其中如果可能,所需项目始终显示为第一个选定项目,否则滚动查看器将滚动到结尾,并在其位置选择目标项目。

The key points are :

关键点是:

  • Use CollectionView on the business side and enable current item synch on the XAML control (IsSynchronizedWithCurrentItem=true)
  • 在业务端使用CollectionView并在XAML控件上启用当前项同步(IsSynchronizedWithCurrentItem = true)
  • Defer the "real" target scroll in order to allow the "Select Last item" to be visualy executed (By using a Dispatcher.BeginInvoke with a low priority)
  • 推迟“真实”目标滚动,以便允许“选择最后一项”被视觉执行(通过使用具有低优先级的Dispatcher.BeginInvoke)

Here is the business logic (This is automatic convertion from C# to VB)

这是业务逻辑(这是从C#到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

And this is the xaml

这就是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  

The accepted answer to this other question shows a different approach to get the first/last visible row of such a grid. You could find out the index of your row and directly scroll there or scroll down row by row until the first visible row matches.

此另一个问题的接受答案显示了获取此类网格的第一个/最后一个可见行的不同方法。您可以找到行的索引并直接在那里滚动或逐行向下滚动,直到第一个可见行匹配。

#3


3  

I Solved this question with following code:

我用以下代码解决了这个问题:

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>

#1


5  

You were on the right track, just try to work with collection view instead of working directly on the datagrid for this kind of needs.

您处于正确的轨道上,只是尝试使用集合视图,而不是直接在数据网格上工作以满足此类需求。

Here is a working example where the desired item is always displayed as first selected item if possible, otherwise the scrollviewer is scrolled to the end and the target item is selected at its position.

这是一个工作示例,其中如果可能,所需项目始终显示为第一个选定项目,否则滚动查看器将滚动到结尾,并在其位置选择目标项目。

The key points are :

关键点是:

  • Use CollectionView on the business side and enable current item synch on the XAML control (IsSynchronizedWithCurrentItem=true)
  • 在业务端使用CollectionView并在XAML控件上启用当前项同步(IsSynchronizedWithCurrentItem = true)
  • Defer the "real" target scroll in order to allow the "Select Last item" to be visualy executed (By using a Dispatcher.BeginInvoke with a low priority)
  • 推迟“真实”目标滚动,以便允许“选择最后一项”被视觉执行(通过使用具有低优先级的Dispatcher.BeginInvoke)

Here is the business logic (This is automatic convertion from C# to VB)

这是业务逻辑(这是从C#到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

And this is the xaml

这就是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  

The accepted answer to this other question shows a different approach to get the first/last visible row of such a grid. You could find out the index of your row and directly scroll there or scroll down row by row until the first visible row matches.

此另一个问题的接受答案显示了获取此类网格的第一个/最后一个可见行的不同方法。您可以找到行的索引并直接在那里滚动或逐行向下滚动,直到第一个可见行匹配。

#3


3  

I Solved this question with following code:

我用以下代码解决了这个问题:

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>