using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace EmbyToolbox.Behaviors; public static class TreeViewScrollSyncBehavior { public static readonly DependencyProperty SyncGroupProperty = DependencyProperty.RegisterAttached( "SyncGroup", typeof(string), typeof(TreeViewScrollSyncBehavior), new PropertyMetadata(string.Empty, OnSyncGroupChanged)); private static readonly Dictionary>> Groups = new(StringComparer.Ordinal); private static bool _syncing; public static string GetSyncGroup(DependencyObject obj) => (string)obj.GetValue(SyncGroupProperty); public static void SetSyncGroup(DependencyObject obj, string value) => obj.SetValue(SyncGroupProperty, value); private static void OnSyncGroupChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not TreeView treeView) { return; } treeView.Loaded -= TreeViewOnLoaded; treeView.Unloaded -= TreeViewOnUnloaded; if (!string.IsNullOrWhiteSpace(e.NewValue as string)) { treeView.Loaded += TreeViewOnLoaded; treeView.Unloaded += TreeViewOnUnloaded; } } private static void TreeViewOnLoaded(object sender, RoutedEventArgs e) { if (sender is not TreeView tree) { return; } var group = GetSyncGroup(tree); if (string.IsNullOrWhiteSpace(group)) { return; } if (!Groups.TryGetValue(group, out var list)) { list = new List>(); Groups[group] = list; } list.Add(new WeakReference(tree)); var scroll = FindScrollViewer(tree); if (scroll is not null) { scroll.ScrollChanged -= OnScrollChanged; scroll.ScrollChanged += OnScrollChanged; } } private static void TreeViewOnUnloaded(object sender, RoutedEventArgs e) { if (sender is not TreeView tree) { return; } var scroll = FindScrollViewer(tree); if (scroll is not null) { scroll.ScrollChanged -= OnScrollChanged; } } private static void OnScrollChanged(object sender, ScrollChangedEventArgs e) { if (_syncing || e.VerticalChange == 0 || sender is not ScrollViewer sourceScroll) { return; } var sourceTree = FindAncestor(sourceScroll); if (sourceTree is null) { return; } var group = GetSyncGroup(sourceTree); if (string.IsNullOrWhiteSpace(group) || !Groups.TryGetValue(group, out var members)) { return; } try { _syncing = true; foreach (var weak in members.ToList()) { if (!weak.TryGetTarget(out var targetTree)) { members.Remove(weak); continue; } if (ReferenceEquals(targetTree, sourceTree)) { continue; } var targetScroll = FindScrollViewer(targetTree); if (targetScroll is not null) { targetScroll.ScrollToVerticalOffset(sourceScroll.VerticalOffset); } } } finally { _syncing = false; } } private static ScrollViewer? FindScrollViewer(DependencyObject root) { if (root is ScrollViewer sv) { return sv; } var count = VisualTreeHelper.GetChildrenCount(root); for (var i = 0; i < count; i++) { var child = VisualTreeHelper.GetChild(root, i); var found = FindScrollViewer(child); if (found is not null) { return found; } } return null; } private static T? FindAncestor(DependencyObject node) where T : DependencyObject { var current = node; while (current is not null) { if (current is T t) { return t; } current = VisualTreeHelper.GetParent(current); } return null; } }