Synchronizing scroll positions for 2 WPF DataGrids

14,672

Solution 1

According to the Microsoft product group, traversing the visual tree to find the ScrollViewer is the recommended method, as explained in their answer on Codeplex.

Solution 2

There is great piece of code to do this:

http://www.codeproject.com/KB/WPF/ScrollSynchronization.aspx

Solution 3

We had this same problem when using the Infragistics grid because it didn't (still doesn't) support frozen columns. So we had two grids side-by-side that were made to look as one. The grid on the left didn't scroll horizontally but the grid on the right did. Poor man's frozen columns.

Anyway, we ended up just reaching into the visual tree and pulling out the ScrollViewer ourselves. Afterall, we knew it was there - it just wasn't exposed by the object model. You could use a similar approach if the WPF grid does not expose the ScrollViewer. Or you could subclass the grid and add the functionality you require to make this work.

Interested in hearing why you need to do this.

Solution 4

This is a great solution. Worked fine for me in WPF.

http://www.codeproject.com/Articles/39244/Scroll-Synchronization

I just made a reference to ScrollSynchronizer dll, added a xml import:

xmlns:scroll="clr-namespace:ScrollSynchronizer"

then just added this to both my datagrids and bobs your uncle:

<DataGrid.Resources>
   <Style TargetType="ScrollViewer">
     <Setter Property="scroll:ScrollSynchronizer.ScrollGroup" Value="Group1" />
   </Style>
</DataGrid.Resources>

Solution 5

You can trick the datagrid to expose its ScrollViewer as public property for each grid, when for example innerGridControl_ScrollChanged() handler called during initialisation of the usercontrol. To expose it you can make your grid in an xaml View file, and then compose two of them in another xaml View. Below code is on the innerGrid.xaml.cs for example:

    public ScrollViewer Scroller { get; set; } // exposed ScrollViewer from the grid
    private bool _isFirstTimeLoaded = true; 

    private void innerGridControl_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (_isFirstTimeLoaded) // just to save the code from casting and assignment after 1st time loaded
        {
            var scroller = (e.OriginalSource) as ScrollViewer;
            Scroller = scroller;
            _isFirstTimeLoaded = false;
        }
    }

on OuterGridView.xaml put an attached event handler definition:

<Views:innerGridView Grid.Row="1" Margin="2,0,2,2" DataContext="{Binding someCollection}" 
                                      x:Name="grid1Control"
                                      ScrollViewer.ScrollChanged="Grid1Attached_ScrollChanged"
                                      ></Views:innerGridView>

<Views:innerGridView Grid.Row="3" Margin="2,0,2,2" DataContext="{Binding someCollection}" 
                                      x:Name="grid2Control"
                                      ScrollViewer.ScrollChanged="Grid2Attached_ScrollChanged"
                                      ></Views:innerGridView>

then access that public ScrollViewer.SetHorizontalOffset(e.HorizontalOffset) method when another scrolling event occur. Below code is in the OuterGridView.xaml.cs on one of the handler definition (

private void Grid1Attached_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (e != null && !e.Handled)
        {
            if (e.HorizontalChange != 0.0)
            {
                grid2Control.Scroller.ScrollToHorizontalOffset(e.HorizontalOffset);
            }
            e.Handled = true;
        }
    }
private void Grid2Attached_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (e != null && !e.Handled)
        {
            if (e.HorizontalChange != 0.0)
            {
                grid1Control.Scroller.ScrollToHorizontalOffset(e.HorizontalOffset);
            }
            e.Handled = true;
        }
    }

Also make sure any other scroll_changed event inside the inner grid (if any, for example if you define a TextBox with default scroller in one of the column data template) has its e.Handled set to true to prevent outer grid's handler processing it (this happened due to default bubbling behaviour of routedevents). Alternatively you can put additional if check on e.OriginalSource or e.Source to filter the scroll event you're intended to process.

Share:
14,672
Philipp Schmid
Author by

Philipp Schmid

Updated on June 14, 2022

Comments

  • Philipp Schmid
    Philipp Schmid almost 2 years

    I am trying to synchronize the horizontal scroll position of 2 WPF DataGrid controls.

    I am subscribing to the ScrollChanged event of the first DataGrid:

    <toolkit:DataGrid x:Name="SourceGrid" ScrollViewer.ScrollChanged="SourceGrid_ScrollChanged">
    

    I have a second DataGrid:

    <toolkit:DataGrid x:Name="TargetGrid">
    

    In the event handler I was attempting to use the IScrollInfo.SetHorizontalOffset, but alas, DataGrid doesn't expose IScrollInfo:

    private void SourceGrid_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        ((IScrollInfo)TargetGrid).SetHorizontalOffset(e.HorizontalOffset);
        // cast to IScrollInfo fails
    }
    

    Is there another way to accomplish this? Or is there another element on TargetGrid that exposes the necessary IScrollInfo to achieve the synchronization of the scroll positions?

    BTW, I am using frozen columns, so I cannot wrap both DataGrid controls with ScrollViewers.

  • Philipp Schmid
    Philipp Schmid over 15 years
    I have source for the WPF Toolkit DataGrid from Codeplex, so I might be able to find it and expose it (not my preferred method). I am stacking 2 grids to get the frozen pane effect (ala Excel).
  • PeterAllenWebb
    PeterAllenWebb almost 15 years
    Yeah. I've done the same thing the same way in the past, though. It just seems like we shouldn't have to hack through the visual tree this way. Just another way that WPF is rough around the edges.
  • Tomáš Kafka
    Tomáš Kafka almost 14 years
    Be careful when the user changes visual themes - controls then get new templates (= new visual trees), and you will be holding reference to a wrong scrollviewer. You should react in OnApplyTemplate, and look-up the actual ScrollViewer every time it is invoked. See msdn.microsoft.com/en-us/library/…