根据需求需要制作一颗树,多列显示,带有标头,支持多选绑定,另外带有一些选中、鼠标移动的效果。实现效果如下:
样式资源代码如下,
这里遇到一个问题,如果修改了TextBlock的Foreground,那么在Trigger里面再修改Foreground,是没有效果的。
这个控件有两个附加依赖项属性,一个是解决多选问题,一个是解决滚动scrollView列表头也一起滚动的问题
还有一个解决展开速度的问题,采用了虚拟化技术,主要代码下面我已经标红
<Window x:Class="TreeGrid.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TreeGrid"
xmlns:my="clr-namespace:TreeGrid"
Title="MainWindow" Height="400" Width="525">
<Window.Resources>
<local:TreeViewLineConverter x:Key="LineConverter"/>
<local:LevelToMarginConverter x:Key="LevelToIndentConverter"/>
<Style x:Key="ExpandCollapseToggleStyle" TargetType="ToggleButton">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid x:Name="Root" Background="Transparent" >
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="OuterBorder"
Storyboard.TargetProperty="Opacity"
To="1" Duration="0:0:0.1" />
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Root"
Storyboard.TargetProperty="Opacity"
To="0.5"
Duration="0" />
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="CheckStates">
<VisualState x:Name="Unchecked" />
<VisualState x:Name="Checked">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="UncheckedVisual"
Storyboard.TargetProperty="Opacity"
To="0" Duration="0" />
<DoubleAnimation Storyboard.TargetName="CheckedVisual"
Storyboard.TargetProperty="Opacity"
To="1" Duration="0" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="OuterBorder"
BorderBrush="{StaticResource MyBrushNormalBorder}"
CornerRadius="3"
BorderThickness="1" Opacity="0.5"
Background="{StaticResource MyBrushNormalBevelBackground}"/>
<Grid Margin="3" >
<Path x:Name="UncheckedVisual" UseLayoutRounding="False"
HorizontalAlignment="Center" Stretch="Uniform"
Data="M0,4 L4,4 L4,0 L6,0 L6,4 L10,4 L10,6 L6,6 L6,10 L4,10 L4,6 L0,6 z"
VerticalAlignment="Center"
Opacity="1" Fill="#FFB5BD24">
</Path>
<Path x:Name="CheckedVisual" UseLayoutRounding="False"
HorizontalAlignment="Center" Stretch="Uniform"
Data="M0,4 L10,4 L10,6 L0,6 z"
VerticalAlignment="Center"
Opacity="0" Fill="#FFB5BD24">
</Path>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="CellTemplate_Name">
<DockPanel >
<ToggleButton x:Name="Expander"
Style="{StaticResource ExpandCollapseToggleStyle}"
Margin="{Binding Level,Converter={StaticResource LevelToIndentConverter}}"
IsChecked="{Binding Path=IsExpanded,RelativeSource={RelativeSource AncestorType= {x:Type TreeViewItem}}}"
ClickMode="Press"/>
<TextBlock Text="{Binding Name}"/>
</DockPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=HasItems,
RelativeSource={RelativeSource
AncestorType={x:Type TreeViewItem}}}"
Value="False">
<Setter TargetName="Expander"
Property="Visibility"
Value="Hidden"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<GridViewColumnCollection x:Key="gvcc">
<GridViewColumn Header="Name"
CellTemplate="{StaticResource CellTemplate_Name}" Width="Auto"/>
<GridViewColumn Header="JobTitle"
DisplayMemberBinding="{Binding JobTitle}" Width="60"/>
<GridViewColumn Header="Age"
DisplayMemberBinding="{Binding Age}" Width="60" />
<GridViewColumn Header="Sex"
DisplayMemberBinding="{Binding Sex}" Width="60"/>
</GridViewColumnCollection>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="MinWidth" Value="20"></Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<StackPanel>
<Border Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="0,2,0,2">
<GridViewRowPresenter x:Name="PART_Header"
Content="{TemplateBinding Header}"
Columns="{StaticResource gvcc}" />
</Border>
<ItemsPresenter x:Name="ItemsHost" />
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded"
Value="false">
<Setter TargetName="ItemsHost"
Property="Visibility"
Value="Collapsed"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader"
Value="false"/>
<Condition Property="Width"
Value="Auto"/>
</MultiTrigger.Conditions>
<Setter TargetName="PART_Header"
Property="MinWidth"
Value="75"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader"
Value="false"/>
<Condition Property="Height"
Value="Auto"/>
</MultiTrigger.Conditions>
<Setter TargetName="PART_Header"
Property="MinHeight"
Value="19"/>
</MultiTrigger>
<Trigger Property="IsEnabled"
Value="false">
<Setter Property="Foreground"
Value="{DynamicResource
{x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="Red"></Setter>
</Trigger>
<Trigger Property="my:TreeViewExtensions.IsSelected" Value="true">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush EndPoint="0.5,1" MappingMode="RelativeToBoundingBox" StartPoint="0.5,0">
<GradientStop Color="#FFC7DFFC" Offset="1"/>
<GradientStop Color="#FF3832B8" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TreeView}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeView}">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel>
<ScrollViewer DockPanel.Dock="Top" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility ="Disabled"
Focusable="False" Core:ScrollSynchronizer.HorizontalScrollGroup="H1">
<GridViewHeaderRowPresenter Columns="{StaticResource gvcc}" DockPanel.Dock="Top" />
</ScrollViewer>
<ScrollViewer HorizontalScrollBarVisibility="Auto" CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}"
VerticalScrollBarVisibility="Auto" Focusable="False"
Core:ScrollSynchronizer.HorizontalScrollGroup="H1">
<VirtualizingStackPanel x:Name="ItemsHost" IsItemsHost="True" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" Height="Auto"/>
</ScrollViewer>
</DockPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TextBlockStyle" TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="Red"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" VerticalAlignment="Center" Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Button Height="30" Width="60" Content="test" Click="Button_Click"></Button>
<Button x:Name="updata" Height="30" Width="60" Content="updata" Click="updata_Click"></Button>
</StackPanel>
<ScrollViewer>
<TextBox x:Name="txt" Height="50"></TextBox>
</ScrollViewer>
</StackPanel>
<TreeView Height="300" Grid.Row="1" Name="_list" BorderThickness="0" VerticalAlignment="Stretch" Background="Transparent" ItemsSource="{Binding Children}" ScrollViewer.CanContentScroll="True" VirtualizingStackPanel.IsVirtualizing="True"
my:TreeViewExtensions.EnableMultiSelect="true" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Auto"
my:TreeViewExtensions.SelectedItems="{Binding SelectedTreeNodes, Mode=TwoWay, NotifyOnTargetUpdated=True}" Margin="0,0,0,50">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Border CornerRadius="0" Margin="1" x:Name="back" MinWidth="70"
Background="Transparent" DataContext="{Binding}" >
<StackPanel Orientation="Horizontal" Margin="2">
<TextBlock Text="{Binding Text}" Margin="2 0"/>
</StackPanel>
</Border>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
两个依赖项属性类如下
多选类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.ComponentModel;
using System.Collections;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace TreeGrid.Resource
{
public class TreeViewExtensions : TreeView
{
/// <summary>
/// Gets the value of the dependency property "EnableMultiSelect".
/// </summary>
/// <param name="obj">Dependency Object</param>
/// <returns></returns>
public static bool GetEnableMultiSelect(DependencyObject obj)
{
return (bool)obj.GetValue(EnableMultiSelectProperty);
}
/// <summary>
/// Sets the value of the dependency property "EnableMultiSelect".
/// </summary>
/// <param name="obj">Dependency Object</param>
/// <param name="value"></param>
public static void SetEnableMultiSelect(DependencyObject obj, bool value)
{
obj.SetValue(EnableMultiSelectProperty, value);
}
// Using a DependencyProperty as the backing store for EnableMultiSelect. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EnableMultiSelectProperty =
DependencyProperty.RegisterAttached("EnableMultiSelect", typeof(bool), typeof(TreeViewExtensions), new FrameworkPropertyMetadata(false)
{
PropertyChangedCallback = EnableMultiSelectChanged,
BindsTwoWayByDefault = true
});
/// <summary>
/// Gets the value of the dependency property "SelectedItems".
/// </summary>
/// <param name="obj">Dependency Object</param>
/// <returns></returns>
public static IList GetSelectedItems(DependencyObject obj)
{
return (IList)obj.GetValue(SelectedItemsProperty);
}
/// <summary>
/// Sets the value of the dependency property "SelectedItems".
/// </summary>
/// <param name="obj">Dependency Object</param>
/// <param name="value"></param>
public static void SetSelectedItems(DependencyObject obj, IList value)
{
obj.SetValue(SelectedItemsProperty, value);
}
// Using a DependencyProperty as the backing store for SelectedItems. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.RegisterAttached("SelectedItems", typeof(IList), typeof(TreeViewExtensions), new PropertyMetadata(null));
/// <summary>
/// Gets the value of the dependency property "AnchorItem".
/// </summary>
/// <param name="obj">Dependency Object</param>
/// <returns></returns>
static TreeViewItem GetAnchorItem(DependencyObject obj)
{
return (TreeViewItem)obj.GetValue(AnchorItemProperty);
}
/// <summary>
/// Sets the value of the dependency property "AnchorItem".
/// </summary>
/// <param name="obj">Dependency Object</param>
/// <param name="value"></param>
static void SetAnchorItem(DependencyObject obj, TreeViewItem value)
{
obj.SetValue(AnchorItemProperty, value);
}
// Using a DependencyProperty as the backing store for AnchorItem. This enables animation, styling, binding, etc...
static readonly DependencyProperty AnchorItemProperty =
DependencyProperty.RegisterAttached("AnchorItem", typeof(TreeViewItem), typeof(TreeViewExtensions), new PropertyMetadata(null));
/// <summary>
/// Gets the value of the dependency property "IsSelected".
/// </summary>
/// <param name="obj">Dependency Object</param>
/// <returns></returns>
public static bool GetIsSelected(DependencyObject obj)
{
return (bool)obj.GetValue(IsSelectedProperty);
}
/// <summary>
/// Sets the value of the dependency property "IsSelected".
/// </summary>
/// <param name="obj">Dependency Object</param>
/// <param name="value"></param>
public static void SetIsSelected(DependencyObject obj, bool value)
{
if (value)
{
GradientStopCollection gradientStopCollection = new GradientStopCollection();
gradientStopCollection.Add(new GradientStop()
{
Color = (Color)ColorConverter.ConvertFromString("#FF303030"),
Offset = 1
});
// gradientStopCollection.Add(new GradientStop()
// {
// Color = (Color)ColorConverter.ConvertFromString("#FF3832B8"),
// Offset = 1
// });
LinearGradientBrush brush = new LinearGradientBrush(gradientStopCollection, new Point(0.5, 0), new Point(0.5, 1));
(obj as TreeViewItem).Background = brush;
}
else
{
(obj as TreeViewItem).Background = Brushes.Transparent;
}
obj.SetValue(IsSelectedProperty, value);
}
// Using a DependencyProperty as the backing store for IsSelected. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.RegisterAttached("IsSelected", typeof(bool), typeof(TreeViewExtensions), new PropertyMetadata(false)
{
PropertyChangedCallback = RealSelectedChanged
});
/// <summary>
/// "EnableMultiSelect" changed event.
/// </summary>
/// <param name="s">Dependency Object</param>
/// <param name="args">Event parameter</param>
static void EnableMultiSelectChanged(DependencyObject s, DependencyPropertyChangedEventArgs args)
{
TreeView tree = (TreeView)s;
var wasEnable = (bool)args.OldValue;
var isEnabled = (bool)args.NewValue;
if (wasEnable)
{
tree.RemoveHandler(TreeViewItem.MouseDownEvent, new MouseButtonEventHandler(ItemClicked));
tree.RemoveHandler(TreeView.KeyDownEvent, new KeyEventHandler(KeyDown));
}
if (isEnabled)
{
tree.AddHandler(TreeViewItem.MouseDownEvent, new MouseButtonEventHandler(ItemClicked), true);
tree.AddHandler(TreeView.KeyDownEvent, new KeyEventHandler(KeyDown));
}
}
/// <summary>
/// Gets TreeView which contains the TreeViewItem.
/// </summary>
/// <param name="item">item</param>
/// <returns>TreeView</returns>
static TreeView GetTree(TreeViewItem item)
{
Func<DependencyObject, DependencyObject> getParent = (o) => VisualTreeHelper.GetParent(o);
FrameworkElement currentItem = item;
while (!(getParent(currentItem) is TreeView))
{
currentItem = (FrameworkElement)getParent(currentItem);
}
return (TreeView)getParent(currentItem);
}
/// <summary>
/// TreeViewItem seleted changed event.
/// </summary>
/// <param name="sender">sender</param>
/// <param name="args">event parameter</param>
static void RealSelectedChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
TreeViewItem item = (TreeViewItem)sender;
var selectedItems = GetSelectedItems(GetTree(item));
if (selectedItems != null)
{
var isSelected = GetIsSelected(item);
if (isSelected)
{
try
{
selectedItems.Add(item.Header);
}
catch (ArgumentException)
{
}
}
else
{
selectedItems.Remove(item.Header);
}
}
}
/// <summary>
/// Key down event.
/// </summary>
/// <param name="sender">sender</param>
/// <param name="e">event parameter</param>
static void KeyDown(object sender, KeyEventArgs e)
{
TreeView tree = (TreeView)sender;
if (e.Key == Key.A && e.KeyboardDevice.Modifiers == ModifierKeys.Control)
{
foreach (var item in GetExpandedTreeViewItems(tree))
{
SetIsSelected(item, true);
}
e.Handled = true;
}
}
/// <summary>
/// Item clicked event.
/// </summary>
/// <param name="sender">sender</param>
/// <param name="e">event parameter</param>
static void ItemClicked(object sender, MouseButtonEventArgs e)
{
TreeViewItem item = FindTreeViewItem(e.OriginalSource);
if (item == null)
{
return;
}
TreeView tree = (TreeView)sender;
var mouseButton = e.ChangedButton;
if (mouseButton != MouseButton.Left)
{
if ((mouseButton == MouseButton.Right) && ((Keyboard.Modifiers & (ModifierKeys.Shift | ModifierKeys.Control)) == ModifierKeys.None))
{
if (GetIsSelected(item))
{
UpdateAnchorAndActionItem(tree, item);
return;
}
MakeSingleSelection(tree, item);
}
return;
}
if ((Keyboard.Modifiers & (ModifierKeys.Shift | ModifierKeys.Control)) != (ModifierKeys.Shift | ModifierKeys.Control))
{
if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
{
MakeToggleSelection(tree, item);
return;
}
if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
{
MakeAnchorSelection(tree, item, true);
return;
}
MakeSingleSelection(tree, item);
return;
}
}
/// <summary>
/// Find TreeViewItem which contains the object.
/// </summary>
/// <param name="obj">obj</param>
/// <returns></returns>
private static TreeViewItem FindTreeViewItem(object obj)
{
DependencyObject dpObj = obj as DependencyObject;
if (dpObj == null)
{
return null;
}
if (dpObj is TreeViewItem)
{
return (TreeViewItem)dpObj;
}
return FindTreeViewItem(VisualTreeHelper.GetParent(dpObj));
}
/// <summary>
/// Gets all expanded TreeViewItems.
/// </summary>
/// <param name="tree">TreeView</param>
/// <returns></returns>
private static IEnumerable<TreeViewItem> GetExpandedTreeViewItems(ItemsControl tree)
{
for (int i = 0; i < tree.Items.Count; i++)
{
var item = (TreeViewItem)tree.ItemContainerGenerator.ContainerFromIndex(i);
if (item == null)
{
continue;
}
yield return item;
// if (item.IsExpanded)
// {
foreach (var subItem in GetExpandedTreeViewItems(item))
{
yield return subItem;
}
// }
}
}
/// <summary>
/// Select by Shift key.
/// </summary>
/// <param name="tree"></param>
/// <param name="actionItem"></param>
/// <param name="clearCurrent"></param>
private static void MakeAnchorSelection(TreeView tree, TreeViewItem actionItem, bool clearCurrent)
{
if (GetAnchorItem(tree) == null)
{
var selectedItems = GetSelectedTreeViewItems(tree);
if (selectedItems.Count > 0)
{
SetAnchorItem(tree, selectedItems[selectedItems.Count - 1]);
}
else
{
SetAnchorItem(tree, GetExpandedTreeViewItems(tree).Skip(3).FirstOrDefault());
}
if (GetAnchorItem(tree) == null)
{
return;
}
}
var anchor = GetAnchorItem(tree);
var items = GetExpandedTreeViewItems(tree);
bool betweenBoundary = false;
foreach (var item in items)
{
bool isBoundary = item == anchor || item == actionItem;
if (isBoundary)
{
betweenBoundary = !betweenBoundary;
}
if (betweenBoundary || isBoundary)
{
SetIsSelected(item, true);
}
else
{
if (clearCurrent)
{
SetIsSelected(item, false);
}
else
{
break;
}
}
}
}
/// <summary>
/// Gets all selected TreeViewItems.
/// </summary>
/// <param name="tree">TreeView</param>
/// <returns></returns>
private static List<TreeViewItem> GetSelectedTreeViewItems(TreeView tree)
{
return GetExpandedTreeViewItems(tree).Where(i => GetIsSelected(i)).ToList();
}
/// <summary>
/// Select by left mouse button.
/// </summary>
/// <param name="tree"></param>
/// <param name="item"></param>
private static void MakeSingleSelection(TreeView tree, TreeViewItem item)
{
foreach (TreeViewItem selectedItem in GetExpandedTreeViewItems(tree))
{
if (selectedItem == null)
{
continue;
}
if (selectedItem != item)
{
SetIsSelected(selectedItem, false);
}
else
{
SetIsSelected(selectedItem, true);
}
}
UpdateAnchorAndActionItem(tree, item);
}
/// <summary>
/// Select by Ctrl key.
/// </summary>
/// <param name="tree">TreeView</param>
/// <param name="item">TreeViewItem</param>
private static void MakeToggleSelection(TreeView tree, TreeViewItem item)
{
SetIsSelected(item, !GetIsSelected(item));
UpdateAnchorAndActionItem(tree, item);
}
/// <summary>
/// Update the Anchor TreeViewItem.
/// </summary>
/// <param name="tree">TreeView</param>
/// <param name="item">TreeViewItem</param>
private static void UpdateAnchorAndActionItem(TreeView tree, TreeViewItem item)
{
SetAnchorItem(tree, item);
}
}
}
滚动条联动类
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace ScrollViewerSynchronization.Core
{
public sealed class ScrollSynchronizer
{
#region Constant(s)
private const string VerticalScrollGroupPropertyName = "VerticalScrollGroup";
private const string HorizontalScrollGroupPropertyName = "HorizontalScrollGroup";
private const string ScrollSyncTypePropertyName = "ScrollSyncType";
#endregion
#region Dependency Propert(y/ies)
#region Declaration(s)
public static readonly DependencyProperty HorizontalScrollGroupProperty =
DependencyProperty.RegisterAttached(HorizontalScrollGroupPropertyName, typeof(string), typeof(ScrollSynchronizer), new PropertyMetadata(string.Empty, OnHorizontalScrollGroupChanged));
public static readonly DependencyProperty VerticalScrollGroupProperty =
DependencyProperty.RegisterAttached(VerticalScrollGroupPropertyName, typeof(string), typeof(ScrollSynchronizer), new PropertyMetadata(string.Empty, OnVerticalScrollGroupChanged));
public static readonly DependencyProperty ScrollSyncTypeProperty =
DependencyProperty.RegisterAttached(ScrollSyncTypePropertyName, typeof(ScrollSyncType), typeof(ScrollSynchronizer), new PropertyMetadata(ScrollSyncType.None, OnScrollSyncTypeChanged));
#endregion
#region Getter(s)/Setter(s)
public static void SetVerticalScrollGroup(DependencyObject obj, string verticalScrollGroup)
{
obj.SetValue(VerticalScrollGroupProperty, verticalScrollGroup);
}
public static string GetVerticalScrollGroup(DependencyObject obj)
{
return (string)obj.GetValue(VerticalScrollGroupProperty);
}
public static void SetHorizontalScrollGroup(DependencyObject obj, string horizontalScrollGroup)
{
obj.SetValue(HorizontalScrollGroupProperty, horizontalScrollGroup);
}
public static string GetHorizontalScrollGroup(DependencyObject obj)
{
return (string)obj.GetValue(HorizontalScrollGroupProperty);
}
public static void SetScrollSyncType(DependencyObject obj, ScrollSyncType scrollSyncType)
{
obj.SetValue(ScrollSyncTypeProperty, scrollSyncType);
}
public static ScrollSyncType GetScrollSyncType(DependencyObject obj)
{
return (ScrollSyncType)obj.GetValue(ScrollSyncTypeProperty);
}
#endregion
#region Event Handler(s)
private static void OnVerticalScrollGroupChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var scrollViewer = d as ScrollViewer;
if (scrollViewer == null)
return;
var newVerticalGroupName = (e.NewValue == DependencyProperty.UnsetValue ? string.Empty : (string)e.NewValue);
var oldVerticalGroupName = (e.NewValue == DependencyProperty.UnsetValue ? string.Empty : (string)e.OldValue);
removeFromVerticalScrollGroup(oldVerticalGroupName, scrollViewer);
addToVerticalScrollGroup(newVerticalGroupName, scrollViewer);
var currentScrollSyncValue = readSyncTypeDPValue(d, ScrollSyncTypeProperty);
if (currentScrollSyncValue == ScrollSyncType.None)
d.SetValue(ScrollSyncTypeProperty, ScrollSyncType.Vertical);
else if (currentScrollSyncValue == ScrollSyncType.Horizontal)
d.SetValue(ScrollSyncTypeProperty, ScrollSyncType.Vertical);
}
private static void OnHorizontalScrollGroupChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var scrollViewer = d as ScrollViewer;
if (scrollViewer == null)
return;
var newHorizontalGroupName = (e.NewValue == DependencyProperty.UnsetValue ? string.Empty : (string)e.NewValue);
var oldHorizontalGroupName = (e.NewValue == DependencyProperty.UnsetValue ? string.Empty : (string)e.OldValue);
removeFromHorizontalScrollGroup(oldHorizontalGroupName, scrollViewer);
addToHorizontalScrollGroup(newHorizontalGroupName, scrollViewer);
var currentScrollSyncValue = readSyncTypeDPValue(d, ScrollSyncTypeProperty);
if (currentScrollSyncValue == ScrollSyncType.None)
d.SetValue(ScrollSyncTypeProperty, ScrollSyncType.Horizontal);
else if (currentScrollSyncValue == ScrollSyncType.Vertical)
d.SetValue(ScrollSyncTypeProperty, ScrollSyncType.Both);
}
private static void OnScrollSyncTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var scrollViewer = d as ScrollViewer;
if (scrollViewer == null)
return;
var verticalGroupName = readStringDPValue(d, VerticalScrollGroupProperty);
var horizontalGroupName = readStringDPValue(d, HorizontalScrollGroupProperty);
var scrollSyncType = ScrollSyncType.None;
try
{
scrollSyncType = (ScrollSyncType)e.NewValue;
}
catch { }
switch (scrollSyncType)
{
case ScrollSyncType.None:
if (!registeredScrollViewers.ContainsKey(scrollViewer))
return;
removeFromVerticalScrollGroup(verticalGroupName, scrollViewer);
removeFromHorizontalScrollGroup(horizontalGroupName, scrollViewer);
registeredScrollViewers.Remove(scrollViewer);
break;
case ScrollSyncType.Horizontal:
removeFromVerticalScrollGroup(verticalGroupName, scrollViewer);
addToHorizontalScrollGroup(horizontalGroupName, scrollViewer);
if (registeredScrollViewers.ContainsKey(scrollViewer))
registeredScrollViewers[scrollViewer] = ScrollSyncType.Horizontal;
else
registeredScrollViewers.Add(scrollViewer, ScrollSyncType.Horizontal);
break;
case ScrollSyncType.Vertical:
removeFromHorizontalScrollGroup(horizontalGroupName, scrollViewer);
addToVerticalScrollGroup(verticalGroupName, scrollViewer);
if (registeredScrollViewers.ContainsKey(scrollViewer))
registeredScrollViewers[scrollViewer] = ScrollSyncType.Vertical;
else
registeredScrollViewers.Add(scrollViewer, ScrollSyncType.Vertical);
break;
case ScrollSyncType.Both:
if (registeredScrollViewers.ContainsKey(scrollViewer))
{
if (registeredScrollViewers[scrollViewer] == ScrollSyncType.Horizontal)
addToVerticalScrollGroup(verticalGroupName, scrollViewer);
else if (registeredScrollViewers[scrollViewer] == ScrollSyncType.Vertical)
addToHorizontalScrollGroup(horizontalGroupName, scrollViewer);
registeredScrollViewers[scrollViewer] = ScrollSyncType.Both;
}
else
{
addToHorizontalScrollGroup(horizontalGroupName, scrollViewer);
addToVerticalScrollGroup(verticalGroupName, scrollViewer);
registeredScrollViewers.Add(scrollViewer, ScrollSyncType.Both);
}
break;
}
}
#endregion
#endregion
#region Variable(s)
private static readonly Dictionary<string, OffSetContainer> verticalScrollGroups = new Dictionary<string, OffSetContainer>();
private static readonly Dictionary<string, OffSetContainer> horizontalScrollGroups = new Dictionary<string, OffSetContainer>();
private static readonly Dictionary<ScrollViewer, ScrollSyncType> registeredScrollViewers = new Dictionary<ScrollViewer, ScrollSyncType>();
#endregion
#region Method(s)
private static void removeFromVerticalScrollGroup(string verticalGroupName, ScrollViewer scrollViewer)
{
if (verticalScrollGroups.ContainsKey(verticalGroupName))
{
verticalScrollGroups[verticalGroupName].ScrollViewers.Remove(scrollViewer);
if (verticalScrollGroups[verticalGroupName].ScrollViewers.Count == 0)
verticalScrollGroups.Remove(verticalGroupName);
}
scrollViewer.ScrollChanged -= ScrollViewer_VerticalScrollChanged;
}
private static void addToVerticalScrollGroup(string verticalGroupName, ScrollViewer scrollViewer)
{
if (verticalScrollGroups.ContainsKey(verticalGroupName))
{
scrollViewer.ScrollToVerticalOffset(verticalScrollGroups[verticalGroupName].Offset);
verticalScrollGroups[verticalGroupName].ScrollViewers.Add(scrollViewer);
}
else
{
verticalScrollGroups.Add(verticalGroupName, new OffSetContainer { ScrollViewers = new List<ScrollViewer> { scrollViewer }, Offset = scrollViewer.VerticalOffset });
}
scrollViewer.ScrollChanged += ScrollViewer_VerticalScrollChanged;
}
private static void removeFromHorizontalScrollGroup(string horizontalGroupName, ScrollViewer scrollViewer)
{
if (horizontalScrollGroups.ContainsKey(horizontalGroupName))
{
horizontalScrollGroups[horizontalGroupName].ScrollViewers.Remove(scrollViewer);
if (horizontalScrollGroups[horizontalGroupName].ScrollViewers.Count == 0)
horizontalScrollGroups.Remove(horizontalGroupName);
}
scrollViewer.ScrollChanged -= ScrollViewer_HorizontalScrollChanged;
}
private static void addToHorizontalScrollGroup(string horizontalGroupName, ScrollViewer scrollViewer)
{
if (horizontalScrollGroups.ContainsKey(horizontalGroupName))
{
scrollViewer.ScrollToHorizontalOffset(horizontalScrollGroups[horizontalGroupName].Offset);
horizontalScrollGroups[horizontalGroupName].ScrollViewers.Add(scrollViewer);
}
else
{
horizontalScrollGroups.Add(horizontalGroupName, new OffSetContainer { ScrollViewers = new List<ScrollViewer> { scrollViewer }, Offset = scrollViewer.HorizontalOffset });
}
scrollViewer.ScrollChanged += ScrollViewer_HorizontalScrollChanged;
}
private static string readStringDPValue(DependencyObject d, DependencyProperty dp)
{
var value = d.ReadLocalValue(dp);
return (value == DependencyProperty.UnsetValue ? string.Empty : value.ToString());
}
private static ScrollSyncType readSyncTypeDPValue(DependencyObject d, DependencyProperty dp)
{
var value = d.ReadLocalValue(dp);
return (value == DependencyProperty.UnsetValue ? ScrollSyncType.None : (ScrollSyncType)value);
}
#endregion
#region Event Handler(s)
private static void ScrollViewer_VerticalScrollChanged(object sender, ScrollChangedEventArgs e)
{
var changedScrollViewer = sender as ScrollViewer;
if (changedScrollViewer == null)
return;
if (e.VerticalChange == 0)
return;
var verticalScrollGroup = readStringDPValue(sender as DependencyObject, VerticalScrollGroupProperty);
if (!verticalScrollGroups.ContainsKey(verticalScrollGroup))
return;
verticalScrollGroups[verticalScrollGroup].Offset = changedScrollViewer.VerticalOffset;
foreach (var scrollViewer in verticalScrollGroups[verticalScrollGroup].ScrollViewers)
{
if (scrollViewer.VerticalOffset == changedScrollViewer.VerticalOffset)
continue;
scrollViewer.ScrollToVerticalOffset(changedScrollViewer.VerticalOffset);
}
}
private static void ScrollViewer_HorizontalScrollChanged(object sender, ScrollChangedEventArgs e)
{
var changedScrollViewer = sender as ScrollViewer;
if (changedScrollViewer == null)
return;
if (e.HorizontalChange == 0)
return;
var horizontalScrollGroup = readStringDPValue(sender as DependencyObject, HorizontalScrollGroupProperty);
if (!horizontalScrollGroups.ContainsKey(horizontalScrollGroup))
return;
horizontalScrollGroups[horizontalScrollGroup].Offset = changedScrollViewer.HorizontalOffset;
foreach (var scrollViewer in horizontalScrollGroups[horizontalScrollGroup].ScrollViewers)
{
if (scrollViewer.HorizontalOffset == changedScrollViewer.HorizontalOffset)
continue;
scrollViewer.ScrollToHorizontalOffset(changedScrollViewer.HorizontalOffset);
}
}
#endregion
#region Class(es)
private class OffSetContainer
{
public double Offset { get; set; }
public List<ScrollViewer> ScrollViewers { get; set; }
}
#endregion
}
public enum ScrollSyncType
{
Both,
Horizontal,
Vertical,
None
}
}