























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>




<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 }
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。