WPF two separated datagrid scroll synchronously both in horizontal and vertical via behavior

Install-Package Microsoft.Xaml.Behaviors.Wpf

 

 

public class SyncDatagridScrollBehavior : Behavior<DataGrid>
{
    private ScrollViewer _sourceScrollViewer, _targetScrollViewer;
    private bool _isSyncScroll = false;
    public DataGrid TargetDG
    {
        get { return (DataGrid)GetValue(TargetDGProperty); }
        set { SetValue(TargetDGProperty, value); }
    }

    // Using a DependencyProperty as the backing store for TargetDG.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty TargetDGProperty =
        DependencyProperty.Register(nameof(TargetDG),
            typeof(DataGrid),
            typeof(SyncDatagridScrollBehavior),
            new PropertyMetadata(null, OnTargetDGChanged));

    private static void OnTargetDGChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behavior = (SyncDatagridScrollBehavior)d;
        if (behavior != null)
        {
            behavior.RegisterEvents();
        }
    }



    public string BehaviorMainTitle
    {
        get { return (string)GetValue(BehaviorMainTitleProperty); }
        set { SetValue(BehaviorMainTitleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for BehaviorMainTitle.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty BehaviorMainTitleProperty =
        DependencyProperty.Register(
            nameof(BehaviorMainTitle),
            typeof(string), 
            typeof(SyncDatagridScrollBehavior), 
            new PropertyMetadata(null));

    private void RegisterEvents()
    {
        if (TargetDG != null)
        {
            _targetScrollViewer = GetScrollViewer(TargetDG);
            if (_targetScrollViewer != null)
            {
                _targetScrollViewer.ScrollChanged += _targetScrollViewer_ScrollChanged;
            }
        }
    }

    private void _targetScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (_isSyncScroll || _sourceScrollViewer == null)
        {
            return;
        }
        _isSyncScroll = true;
        try
        {
            _sourceScrollViewer.ScrollToVerticalOffset(e.VerticalOffset);
            _sourceScrollViewer.ScrollToHorizontalOffset(e.HorizontalOffset);
            BehaviorMainTitle = $"{DateTime.Now},VerticalOffset:{e.VerticalOffset},HorizontalOffset:{e.HorizontalOffset}";
        }
        finally
        {
            _isSyncScroll = false;
        }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Loaded += AssociatedObject_Loaded;
    }

    private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        _sourceScrollViewer = GetScrollViewer(AssociatedObject);
        if (_sourceScrollViewer != null)
        {
            _sourceScrollViewer.ScrollChanged += _sourceScrollViewer_ScrollChanged;
        }
    }

    private void _sourceScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (_isSyncScroll || _targetScrollViewer == null)
        {
            return;
        }

        _isSyncScroll = true;
        try
        {
            _targetScrollViewer.ScrollToVerticalOffset(e.VerticalOffset);
            _targetScrollViewer.ScrollToHorizontalOffset(e.HorizontalOffset);
            BehaviorMainTitle = $"{DateTime.Now},VerticalOffset:{e.VerticalOffset},HorizontalOffset:{e.HorizontalOffset}";
        }
        finally
        {
            _isSyncScroll = false;
        }
    }

    private ScrollViewer GetScrollViewer(DependencyObject dpObj)
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dpObj); i++)
        {
            var child = VisualTreeHelper.GetChild(dpObj, i);
            if (child is ScrollViewer scrollViewer)
            {
                return scrollViewer;
            }

            var result = GetScrollViewer(child);
            if (result != null)
            {
                return result;
            }
        }
        return null;
    }
}



<Window x:Class="WpfApp17.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:local="clr-namespace:WpfApp17"
        mc:Ignorable="d"
        Title="{Binding MainTitle}"
        WindowState="Maximized">
    <Window.DataContext>
        <local:MainVM/>
    </Window.DataContext>

    <Window.Resources>
        <Style TargetType="DataGridRow">
            <Setter Property="FontSize" Value="30"/>
            <Style.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Foreground" Value="Red"/>
                </Trigger>
            </Style.Triggers>
        </Style>

        <DataTemplate DataType="{x:Type local:Book}" 
              x:Key="DataGridDataTemplate">
            <DataTemplate.Resources>
                <Style TargetType="TextBlock">
                    <Setter Property="Margin" Value="10,0"/>
                    <Setter Property="FontSize" Value="20"/>
                </Style>
            </DataTemplate.Resources>
            <Border BorderBrush="LightBlue"
            BorderThickness="2"
            Margin="5,0">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Row="0" Grid.Column="0" >Id:<Run Text="{Binding Id}"></Run></TextBlock>
                    <TextBlock Grid.Row="0" Grid.Column="1">Name:<Run Text="{Binding Name}"></Run></TextBlock>
                    <TextBlock Grid.Row="0" Grid.Column="2">ISBN:<Run Text="{Binding ISBN}"></Run></TextBlock>
                    <TextBlock Grid.Row="0" Grid.Column="3">Comment:<Run Text="{Binding Comment}"></Run></TextBlock>
                    <TextBlock Grid.Row="1" Grid.Column="0">Content:<Run Text="{Binding Content}"></Run></TextBlock>
                    <TextBlock Grid.Row="1" Grid.Column="1">Summary:<Run Text="{Binding Summary}"></Run></TextBlock>
                    <TextBlock Grid.Row="1" Grid.Column="2">Title:<Run Text="{Binding Title}"></Run></TextBlock>
                    <TextBlock Grid.Row="1" Grid.Column="3">Topic:<Run Text="{Binding Topic}"></Run></TextBlock>
                </Grid>
            </Border>
        </DataTemplate>
        
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <DataGrid x:Name="LeftDG"
          Grid.Column="0"
          ItemsSource="{Binding Books}"
          VirtualizingPanel.IsVirtualizing="True"
          VirtualizingPanel.VirtualizationMode="Recycling"
          VirtualizingPanel.CacheLength="5,5"
          SnapsToDevicePixels="True"
          UseLayoutRounding="True"
          ScrollViewer.CanContentScroll="True"
          ScrollViewer.IsDeferredScrollingEnabled="False"
          AutoGenerateColumns="False"
          CanUserAddRows="False">
            <DataGrid.Columns>
                <DataGridTemplateColumn>
                    <DataGridTemplateColumn.CellTemplate>
                        <Binding Source="{StaticResource DataGridDataTemplate}"/>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
            <behavior:Interaction.Behaviors>
                <local:SyncDatagridScrollBehavior TargetDG="{Binding ElementName=RightDG}"
                                                  BehaviorMainTitle="{Binding MainTitle,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
            </behavior:Interaction.Behaviors>
        </DataGrid>

        <DataGrid x:Name="RightDG"
              Grid.Column="1"
              ItemsSource="{Binding Books}"
              VirtualizingPanel.IsVirtualizing="True"
              VirtualizingPanel.VirtualizationMode="Recycling"
              VirtualizingPanel.CacheLength="5,5"
              SnapsToDevicePixels="True"
              UseLayoutRounding="True"
              ScrollViewer.CanContentScroll="True"
              ScrollViewer.IsDeferredScrollingEnabled="False"
              AutoGenerateColumns="False"
              CanUserAddRows="False">
            <DataGrid.Columns>
                <DataGridTemplateColumn>
                    <DataGridTemplateColumn.CellTemplate>
                        <Binding Source="{StaticResource DataGridDataTemplate}"/>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
            <behavior:Interaction.Behaviors>
                <local:SyncDatagridScrollBehavior TargetDG="{Binding ElementName=LeftDG}"
                                                  BehaviorMainTitle="{Binding MainTitle,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
            </behavior:Interaction.Behaviors>
        </DataGrid>
    </Grid>
</Window>

 

 

 

 

image

 

 

 

 

 

 

image

 

 

 

 

 

image

 

 

 

image

 

 

<Window x:Class="WpfApp17.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:local="clr-namespace:WpfApp17"
        mc:Ignorable="d"
        Title="{Binding MainTitle}"
        WindowState="Maximized">
    <Window.DataContext>
        <local:MainVM/>
    </Window.DataContext>

    <Window.Resources>
        <Style TargetType="DataGridRow">
            <Setter Property="FontSize" Value="30"/>
            <Style.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Foreground" Value="Red"/>
                </Trigger>
            </Style.Triggers>
        </Style>

        <DataTemplate DataType="{x:Type local:Book}" 
              x:Key="DataGridDataTemplate">
            <DataTemplate.Resources>
                <Style TargetType="TextBlock">
                    <Setter Property="Margin" Value="10,0"/>
                    <Setter Property="FontSize" Value="20"/>
                </Style>
            </DataTemplate.Resources>
            <Border BorderBrush="LightBlue"
            BorderThickness="2"
            Margin="5,0">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Row="0" Grid.Column="0" >Id:<Run Text="{Binding Id}"></Run></TextBlock>
                    <TextBlock Grid.Row="0" Grid.Column="1">Name:<Run Text="{Binding Name}"></Run></TextBlock>
                    <TextBlock Grid.Row="0" Grid.Column="2">ISBN:<Run Text="{Binding ISBN}"></Run></TextBlock>
                    <TextBlock Grid.Row="0" Grid.Column="3">Comment:<Run Text="{Binding Comment}"></Run></TextBlock>
                    <TextBlock Grid.Row="1" Grid.Column="0">Content:<Run Text="{Binding Content}"></Run></TextBlock>
                    <TextBlock Grid.Row="1" Grid.Column="1">Summary:<Run Text="{Binding Summary}"></Run></TextBlock>
                    <TextBlock Grid.Row="1" Grid.Column="2">Title:<Run Text="{Binding Title}"></Run></TextBlock>
                    <TextBlock Grid.Row="1" Grid.Column="3">Topic:<Run Text="{Binding Topic}"></Run></TextBlock>
                </Grid>
            </Border>
        </DataTemplate>
        
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <DataGrid x:Name="LeftDG"
          Grid.Column="0"
          ItemsSource="{Binding Books}"
          VirtualizingPanel.IsVirtualizing="True"
          VirtualizingPanel.VirtualizationMode="Recycling"
          VirtualizingPanel.CacheLength="5,5"
          SnapsToDevicePixels="True"
          UseLayoutRounding="True"
          ScrollViewer.CanContentScroll="True"
          ScrollViewer.IsDeferredScrollingEnabled="False"
          AutoGenerateColumns="False"
          CanUserAddRows="False">
            <DataGrid.Columns>
                <DataGridTemplateColumn>
                    <DataGridTemplateColumn.CellTemplate>
                        <Binding Source="{StaticResource DataGridDataTemplate}"/>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
            <behavior:Interaction.Behaviors>
                <local:SyncDatagridScrollBehavior TargetDG="{Binding ElementName=RightDG}"
                                                  BehaviorMainTitle="{Binding MainTitle,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
            </behavior:Interaction.Behaviors>
        </DataGrid>

        <DataGrid x:Name="RightDG"
              Grid.Column="1"
              ItemsSource="{Binding Books}"
              VirtualizingPanel.IsVirtualizing="True"
              VirtualizingPanel.VirtualizationMode="Recycling"
              VirtualizingPanel.CacheLength="5,5"
              SnapsToDevicePixels="True"
              UseLayoutRounding="True"
              ScrollViewer.CanContentScroll="True"
              ScrollViewer.IsDeferredScrollingEnabled="False"
              AutoGenerateColumns="False"
              CanUserAddRows="False">
            <DataGrid.Columns>
                <DataGridTemplateColumn>
                    <DataGridTemplateColumn.CellTemplate>
                        <Binding Source="{StaticResource DataGridDataTemplate}"/>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
            <behavior:Interaction.Behaviors>
                <local:SyncDatagridScrollBehavior TargetDG="{Binding ElementName=LeftDG}"
                                                  BehaviorMainTitle="{Binding MainTitle,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
            </behavior:Interaction.Behaviors>
        </DataGrid>
    </Grid>
</Window>



using Microsoft.Xaml.Behaviors;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp17
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }

    public class MainVM : INotifyPropertyChanged
    {
        private static long id = 1;
        private static (long, long) GetStartEnd(long interval)
        {
            long end = Interlocked.Add(ref id, interval);
            long start = end - interval;
            return (start, end);
        }

        public MainVM()
        {
            if (DesignerProperties.GetIsInDesignMode(new DependencyObject()))
            {
                return;
            }

            Books = new ObservableCollection<Book>();
            _ = InitBooksAsync();
        }

        private async Task InitBooksAsync(long cnt = 1000000)
        {
            var (start, end) = GetStartEnd(cnt);
            Books.Clear();

            await Task.Run(async () =>
            {
                List<Book> bks = new List<Book>();
                for (long i = start; i < end; i++)
                {
                    bks.Add(new Book()
                    {
                        Id = i,
                        Name = $"Name_{i}",
                        ISBN = $"ISBN_{i}_{Guid.NewGuid():N}",
                        Comment = $"Comment_{i}",
                        Content = $"Content_{i}",
                        Summary = $"Summary_{i}",
                        Title = $"Title_{i}",
                        Topic = $"Topic_{i}"
                    });
                }

                Application.Current?.Dispatcher.InvokeAsync(() =>
                {
                    Books = new ObservableCollection<Book>(bks);
                });
                await Task.Delay(0);
            });
        }

        private string mainTitle="";
        public string MainTitle
        {
            get
            {
                return mainTitle;
            }
            set
            {
                if(value!=mainTitle)
                {
                    mainTitle = value;
                    OnPropertyChanged();
                }
            }
        }

        private ObservableCollection<Book> books;
        public ObservableCollection<Book> Books
        {
            get
            {
                return books;
            }
            set
            {
                if (value != books)
                {
                    books = value;
                    OnPropertyChanged();
                }
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] string propName = "")
        {
            var handler = Volatile.Read(ref PropertyChanged);
            if (handler != null)
            {
                handler?.Invoke(this, new PropertyChangedEventArgs(propName));
            }
        }
    }

    public class SyncDatagridScrollBehavior : Behavior<DataGrid>
    {
        private ScrollViewer _sourceScrollViewer, _targetScrollViewer;
        private bool _isSyncScroll = false;
        public DataGrid TargetDG
        {
            get { return (DataGrid)GetValue(TargetDGProperty); }
            set { SetValue(TargetDGProperty, value); }
        }

        // Using a DependencyProperty as the backing store for TargetDG.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TargetDGProperty =
            DependencyProperty.Register(nameof(TargetDG),
                typeof(DataGrid),
                typeof(SyncDatagridScrollBehavior),
                new PropertyMetadata(null, OnTargetDGChanged));

        private static void OnTargetDGChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var behavior = (SyncDatagridScrollBehavior)d;
            if (behavior != null)
            {
                behavior.RegisterEvents();
            }
        }



        public string BehaviorMainTitle
        {
            get { return (string)GetValue(BehaviorMainTitleProperty); }
            set { SetValue(BehaviorMainTitleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for BehaviorMainTitle.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty BehaviorMainTitleProperty =
            DependencyProperty.Register(
                nameof(BehaviorMainTitle),
                typeof(string), 
                typeof(SyncDatagridScrollBehavior), 
                new PropertyMetadata(null));

        private void RegisterEvents()
        {
            if (TargetDG != null)
            {
                _targetScrollViewer = GetScrollViewer(TargetDG);
                if (_targetScrollViewer != null)
                {
                    _targetScrollViewer.ScrollChanged += _targetScrollViewer_ScrollChanged;
                }
            }
        }

        private void _targetScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            if (_isSyncScroll || _sourceScrollViewer == null)
            {
                return;
            }
            _isSyncScroll = true;
            try
            {
                _sourceScrollViewer.ScrollToVerticalOffset(e.VerticalOffset);
                _sourceScrollViewer.ScrollToHorizontalOffset(e.HorizontalOffset);
                BehaviorMainTitle = $"{DateTime.Now},VerticalOffset:{e.VerticalOffset},HorizontalOffset:{e.HorizontalOffset}";
            }
            finally
            {
                _isSyncScroll = false;
            }
        }

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.Loaded += AssociatedObject_Loaded;
        }

        private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
        {
            _sourceScrollViewer = GetScrollViewer(AssociatedObject);
            if (_sourceScrollViewer != null)
            {
                _sourceScrollViewer.ScrollChanged += _sourceScrollViewer_ScrollChanged;
            }
        }

        private void _sourceScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            if (_isSyncScroll || _targetScrollViewer == null)
            {
                return;
            }

            _isSyncScroll = true;
            try
            {
                _targetScrollViewer.ScrollToVerticalOffset(e.VerticalOffset);
                _targetScrollViewer.ScrollToHorizontalOffset(e.HorizontalOffset);
                BehaviorMainTitle = $"{DateTime.Now},VerticalOffset:{e.VerticalOffset},HorizontalOffset:{e.HorizontalOffset}";
            }
            finally
            {
                _isSyncScroll = false;
            }
        }

        private ScrollViewer GetScrollViewer(DependencyObject dpObj)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dpObj); i++)
            {
                var child = VisualTreeHelper.GetChild(dpObj, i);
                if (child is ScrollViewer scrollViewer)
                {
                    return scrollViewer;
                }

                var result = GetScrollViewer(child);
                if (result != null)
                {
                    return result;
                }
            }
            return null;
        }
    }

    public class Book
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public string ISBN { get; set; }
        public string Comment { get; set; }
        public string Content { get; set; }
        public string Summary { get; set; }
        public string Title { get; set; }
        public string Topic { get; set; }
    }
}

 

 

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Microsoft.Xaml.Behaviors;

public class SyncDataGridScrollBehavior : Behavior<DataGrid>
{
    private ScrollViewer _sourceScrollViewer;
    private ScrollViewer _targetScrollViewer;
    private bool _isSync = false;

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Loaded += AssociatedObject_Loaded;
        AssociatedObject.Unloaded += AssociatedObject_Unloaded;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.Loaded -= AssociatedObject_Loaded;
        AssociatedObject.Unloaded -= AssociatedObject_Unloaded;
        UnsubscribeSource();
        UnsubscribeTarget();
    }

    #region TargetDataGrid Dependency Property

    public DataGrid TargetDataGrid
    {
        get => (DataGrid)GetValue(TargetDataGridProperty);
        set => SetValue(TargetDataGridProperty, value);
    }

    public static readonly DependencyProperty TargetDataGridProperty =
        DependencyProperty.Register(nameof(TargetDataGrid),
            typeof(DataGrid),
            typeof(SyncDataGridScrollBehavior),
            new PropertyMetadata(null, OnTargetDataGridChanged));

    private static void OnTargetDataGridChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behavior = (SyncDataGridScrollBehavior)d;
        behavior?.OnTargetDataGridChanged((DataGrid)e.OldValue, (DataGrid)e.NewValue);
    }

    private void OnTargetDataGridChanged(DataGrid oldValue, DataGrid newValue)
    {
        if (oldValue != null)
        {
            oldValue.Loaded -= TargetDataGrid_Loaded;
            UnsubscribeTarget();
        }

        if (newValue != null)
        {
            if (newValue.IsLoaded)
                SubscribeTargetScrollViewer(newValue);
            else
                newValue.Loaded += TargetDataGrid_Loaded;
        }
    }

    #endregion

    #region Event Handlers

    private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        SubscribeSourceScrollViewer(AssociatedObject);

        if (TargetDataGrid != null && TargetDataGrid.IsLoaded)
            SubscribeTargetScrollViewer(TargetDataGrid);
    }

    private void AssociatedObject_Unloaded(object sender, RoutedEventArgs e)
    {
        UnsubscribeSource();
        UnsubscribeTarget();
    }

    private void TargetDataGrid_Loaded(object sender, RoutedEventArgs e)
    {
        var target = sender as DataGrid;
        if (target == null) return;

        target.Loaded -= TargetDataGrid_Loaded;
        SubscribeTargetScrollViewer(target);
    }

    #endregion

    #region Subscribe / Unsubscribe Helpers

    private void SubscribeSourceScrollViewer(DataGrid dataGrid)
    {
        UnsubscribeSource();
        _sourceScrollViewer = FindScrollViewer(dataGrid);
        if (_sourceScrollViewer != null)
            _sourceScrollViewer.ScrollChanged += SourceScrollViewer_ScrollChanged;
    }

    private void SubscribeTargetScrollViewer(DataGrid dataGrid)
    {
        UnsubscribeTarget();
        _targetScrollViewer = FindScrollViewer(dataGrid);
        if (_targetScrollViewer != null)
            _targetScrollViewer.ScrollChanged += TargetScrollViewer_ScrollChanged;
    }

    private void UnsubscribeSource()
    {
        if (_sourceScrollViewer == null) return;
        _sourceScrollViewer.ScrollChanged -= SourceScrollViewer_ScrollChanged;
        _sourceScrollViewer = null;
    }

    private void UnsubscribeTarget()
    {
        if (_targetScrollViewer == null) return;
        _targetScrollViewer.ScrollChanged -= TargetScrollViewer_ScrollChanged;
        _targetScrollViewer = null;
    }

    #endregion

    #region Scroll Sync Core Logic

    private void SourceScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (_isSync || _targetScrollViewer == null)
            return;

        _isSync = true;
        try
        {
            _targetScrollViewer.ScrollToVerticalOffset(e.VerticalOffset);
            _targetScrollViewer.ScrollToHorizontalOffset(e.HorizontalOffset);
        }
        finally
        {
            _isSync = false;
        }
    }

    private void TargetScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (_isSync || _sourceScrollViewer == null)
            return;

        _isSync = true;
        try
        {
            _sourceScrollViewer.ScrollToVerticalOffset(e.VerticalOffset);
            _sourceScrollViewer.ScrollToHorizontalOffset(e.HorizontalOffset);
        }
        finally
        {
            _isSync = false;
        }
    }

    #endregion

    #region Visual Tree Search Helpers

    /// <summary>
    /// Recursively locate ScrollViewer inside visual tree
    /// </summary>
    private ScrollViewer FindScrollViewer(DependencyObject parent)
    {
        if (parent == null) return null;

        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
        {
            var child = VisualTreeHelper.GetChild(parent, i);

            if (child is ScrollViewer sv)
                return sv;

            var nestedResult = FindScrollViewer(child);
            if (nestedResult != null)
                return nestedResult;
        }
        return null;
    }

    /// <summary>
    /// Optimized lookup for DataGrid internal ScrollViewer
    /// </summary>
    private ScrollViewer FindDataGridScrollViewer(DataGrid dataGrid)
    {
        if (dataGrid == null) return null;

        var scrollViewer = FindScrollViewer(dataGrid);
        if (scrollViewer != null) return scrollViewer;

        dataGrid.ApplyTemplate();
        return FindScrollViewer(dataGrid);
    }

    #endregion
}

 

posted @ 2026-07-17 21:57  FredGrit  阅读(8)  评论(0)    收藏  举报