WPF数据表格如何滚动以将选定项显示在顶部

11

我有一个包含许多项的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的底部,然后再滚动到所选项,这时所选项会显示在DataGrid的顶部。

我的问题是,滚动到DataGrid底部可以正常工作,但随后滚动到所选项并不总是有效。

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

3个回答

6
你已经在正确的路上了,只需要尝试使用CollectionView而不是直接在Datagrid上操作来满足这类需求。
以下是一个工作示例,在此示例中,如果可能的话,所需的项目始终显示为第一个选择的项目,否则ScrollViewer会滚动到末尾,并在其位置选择目标项。
关键点如下:
- 在业务方面使用CollectionView,并在XAML控件上启用当前项目同步(IsSynchronizedWithCurrentItem=true) - 推迟“真正”的目标滚动,以允许“选择最后一项”在视觉上执行(使用具有低优先级的Dispatcher.BeginInvoke)
这是业务逻辑(这是从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

以下是 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>

4
我用下面的代码解决了这个问题:
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>

这对我很有帮助,但根据微软文档,Move()实际上是将对象在集合中重新定位,这不是我想要的;即,我需要行的顺序保持不变。但还是感谢关于ScrollIntoView()的建议。 - AdvApp

3

对于类似的网格,获取第一个/最后一个可见行的另一种方法可以在这个问题的被接受答案中看到。 您可以找出行的索引并直接滚动到该位置,或者逐行向下滚动,直到第一个可见行匹配。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接