鉴于最近有新的答案发布在这个问题上,我将用MVVM纯解决方案来加入我的观点。
给定perTreeViewItemViewModelBase作为树形视图项数据的基类,您可以使用附加属性在TreeView上创建一个可绑定的选定项属性。
public class perTreeViewHelper : Behavior<TreeView>
{
public object BoundSelectedItem
{
get { return GetValue(BoundSelectedItemProperty); }
set { SetValue(BoundSelectedItemProperty, value); }
}
public static readonly DependencyProperty BoundSelectedItemProperty =
DependencyProperty.Register("BoundSelectedItem",
typeof(object),
typeof(perTreeViewHelper),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnBoundSelectedItemChanged));
private static void OnBoundSelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var item = args.NewValue as perTreeViewItemViewModelBase;
if (item != null)
item.IsSelected = true;
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
base.OnDetaching();
}
private void OnTreeViewSelectedItemChanged(object obj, RoutedPropertyChangedEventArgs<object> args)
{
BoundSelectedItem = args.NewValue;
}
}
一个辅助类用于处理TreeViewItems的滚动到视图中。有两种不同的情况:
- 当选择一个项目时
- 当展开一个项目时,树会滚动以显示尽可能多的子项
请注意使用调度程序优先级,这可以确保在我们尝试将其滚动到视图中之前,任何虚拟化的项都已完全形成。
public static class perTreeViewItemHelper
{
public static bool GetBringSelectedItemIntoView(TreeViewItem treeViewItem)
{
return (bool)treeViewItem.GetValue(BringSelectedItemIntoViewProperty);
}
public static void SetBringSelectedItemIntoView(TreeViewItem treeViewItem, bool value)
{
treeViewItem.SetValue(BringSelectedItemIntoViewProperty, value);
}
public static readonly DependencyProperty BringSelectedItemIntoViewProperty =
DependencyProperty.RegisterAttached(
"BringSelectedItemIntoView",
typeof(bool),
typeof(perTreeViewItemHelper),
new UIPropertyMetadata(false, BringSelectedItemIntoViewChanged));
private static void BringSelectedItemIntoViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (!(args.NewValue is bool))
return;
var item = obj as TreeViewItem;
if (item == null)
return;
if ((bool)args.NewValue)
item.Selected += OnTreeViewItemSelected;
else
item.Selected -= OnTreeViewItemSelected;
}
private static void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
{
var item = e.OriginalSource as TreeViewItem;
item?.BringIntoView();
e.Handled = true;
}
public static bool GetBringExpandedChildrenIntoView(TreeViewItem treeViewItem)
{
return (bool)treeViewItem.GetValue(BringExpandedChildrenIntoViewProperty);
}
public static void SetBringExpandedChildrenIntoView(TreeViewItem treeViewItem, bool value)
{
treeViewItem.SetValue(BringExpandedChildrenIntoViewProperty, value);
}
public static readonly DependencyProperty BringExpandedChildrenIntoViewProperty =
DependencyProperty.RegisterAttached(
"BringExpandedChildrenIntoView",
typeof(bool),
typeof(perTreeViewItemHelper),
new UIPropertyMetadata(false, BringExpandedChildrenIntoViewChanged));
private static void BringExpandedChildrenIntoViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (!(args.NewValue is bool))
return;
var item = obj as TreeViewItem;
if (item == null)
return;
if ((bool)args.NewValue)
item.Expanded += OnTreeViewItemExpanded;
else
item.Expanded -= OnTreeViewItemExpanded;
}
private static void OnTreeViewItemExpanded(object sender, RoutedEventArgs e)
{
var item = e.OriginalSource as TreeViewItem;
if (item == null)
return;
Action action = () =>
{
var lastChild = item.ItemContainerGenerator.ContainerFromIndex(item.Items.Count - 1) as TreeViewItem;
lastChild?.BringIntoView();
};
item.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
action = () => { item.BringIntoView(); };
item.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
e.Handled = true;
}
}
这个辅助类可以被包含在TreeView控件的样式中。
<Style x:Key="perExpandCollapseToggleStyle" TargetType="ToggleButton">
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid Width="10"
Height="10"
Background="Transparent">
<Path x:Name="ExpanderGlyph"
Margin="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Data="M 0,3 L 0,5 L 3,5 L 3,8 L 5,8 L 5,5 L 8,5 L 8,3 L 5,3 L 5,0 L 3,0 L 3,3 z"
Fill="LightGreen"
Stretch="None" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="ExpanderGlyph" Property="Data" Value="M 0,0 M 8,8 M 0,3 L 0,5 L 8,5 L 8,3 z" />
<Setter TargetName="ExpanderGlyph" Property="Fill" Value="Red" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ExpanderGlyph" Property="Fill" Value="}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="perTreeViewItemContainerStyle"
TargetType="">
<!-- Link the properties of perTreeViewItemViewModelBase to the corresponding ones on the TreeViewItem -->
<Setter Property="IsExpanded" Value="" />
<Setter Property="IsSelected" Value="" />
<Setter Property="IsEnabled" Value="" />
<!-- Include the two "Scroll into View" behaviors -->
<Setter Property="vhelp:perTreeViewItemHelper.BringSelectedItemIntoView" Value="True" />
<Setter Property="vhelp:perTreeViewItemHelper.BringExpandedChildrenIntoView" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
MinWidth="14" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ToggleButton x:Name="Expander"
Grid.Row="0"
Grid.Column="0"
ClickMode="Press"
IsChecked="}"
Style="" />
<Border x:Name="PART_Border"
Grid.Row="0"
Grid.Column="1"
Padding=""
Background=""
BorderBrush=""
BorderThickness="">
<ContentPresenter x:Name="PART_Header"
Margin="0,2"
HorizontalAlignment=""
ContentSource="Header" />
</Border>
<ItemsPresenter x:Name="ItemsHost"
Grid.Row="1"
Grid.Column="1" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="false">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="HasItems" Value="false">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden" />
</Trigger>
<!-- Use the same colors for a selected item, whether the TreeView is focussed or not -->
<Trigger Property="IsSelected" Value="true">
<Setter TargetName="PART_Border" Property="Background" Value="}" />
<Setter Property="Foreground" Value="}" />
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="">
<Setter Property="ItemContainerStyle" Value="" />
</Style>
我在最近的博客文章中更详细地介绍了这个问题。