I have a DataGrid with many items and I need to programmatically scroll to the SelectedItem
. I have searched on StackOverflow and Google, and it seems the solution is ScrollIntoView, as follows:
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:
'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.
My problem is that scrolling to the end of the DataGrid works well, but subsequently scrolling to the selected item doesn't always work.
How can I resolve this issue, or is there any other alternative strategy for scrolling to a specific record in the top position?
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>
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 :
IsSynchronizedWithCurrentItem=true
)Dispatcher.BeginInvoke
with a low priority)Here is the business logic (This is automatic convertion from C# to 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
<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>
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With