当项目添加到绑定的 observablecollection 时,WPF 树视图可见性转换器不更新
WPF tree view visibility converters not updating when item is added to bound observable collection
我构建了一个绑定到可观察集合的树视图,并在每个树视图项之间建立了连接线。正在使用的视图模型实现了 INotifyPropertyChanged,我正在使用 PropertyChanged.Fody 进行编织。树视图绑定到集合并且正在更新,除了一件事。当我在运行时向列表添加新项目时,UI 似乎没有正确更新。我已经尝试了所有我能找到的在网上搜索如何强制更新 UI 的方法,而无需在添加根项时发送命令来重建整个树,这确实有效,但是必须有另一种方式我没有找到。
我正在使用 Ninject 进行依赖注入。
我会把所有的代码放在我的问题下面,以供参考。同样,所有这些都运行良好,直到在运行时将一个项目添加到集合中。一旦添加到集合中,该项目就会添加并在树视图中可见,但最后一行转换器不会正确更新所有图形。
考虑下图:
添加项目后,现在成为倒数第二个节点,他的连接线可见性不会更新,他仍然认为他是分支上的最后一个。我已经尝试了我能找到的所有类型的 UI 刷新方法,但没有任何效果。我在这里遗漏了一些东西,但我对 WPF 还很陌生。任何人都可以提供的任何建议将不胜感激。谢谢!
这是我最初构建树视图的方式,效果很好:
ProjectHelpers.JsonObject = JObject.Parse(File.ReadAllText(ProjectPath.BaseDataFullPath));
//-- Get the channels, which are the top level tree elements
var children = ProjectHelpers.GetChannels();
//-- add the channels to the application channel collection
IoC.Application.Channels = new ObservableCollection<ProjectTreeItemViewModel>();
foreach(var c in children)
IoC.Application.Channels.Add(new ProjectTreeItemViewModel(c.Path, ProjectItemType.Channel));
其中包含 class:
/// <summary>
/// The view model for the main project tree view
/// </summary>
public class ProjectTreeViewModel : BaseViewModel
{
/// <summary>
/// Name of the image displayed above the tree view UI
/// </summary>
public string RootImageName => "blink";
/// <summary>
/// Default constructor
/// </summary>
public ProjectTreeViewModel()
{
BuildProjectTree();
}
#region Handlers : Building project data tree
/// <summary>
/// Builds the entire project tree
/// </summary>
public void BuildProjectTree()
{
ProjectHelpers.JsonObject = JObject.Parse(File.ReadAllText(ProjectPath.BaseDataFullPath));
//-- Get the channels, which are the top level tree elements
var children = ProjectHelpers.GetChannels();
//-- add the channels to the application channel collection
IoC.Application.Channels = new ObservableCollection<ProjectTreeItemViewModel>();
foreach(var c in children)
IoC.Application.Channels.Add(new ProjectTreeItemViewModel(c.Path, ProjectItemType.Channel));
}
#endregion
}
添加到可观察集合的项目的视图模型
/// <summary>
/// The view model that represents an item within the tree view
/// </summary>
public class ProjectTreeItemViewModel : BaseViewModel
{
/// <summary>
/// Default constructor
/// </summary>
/// <param name="path">The JSONPath for the item</param>
/// <param name="type">The type of project item type</param>
public ProjectTreeItemViewModel(string path = "", ProjectItemType type = ProjectItemType.Channel)
{
//-- Create commands
ExpandCommand = new RelayCommand(Expand);
GetNodeDataCommand = new RelayCommand(GetNodeData);
FullPath = path;
Type = type;
//-- Setup the children as needed
ClearChildren();
}
#region Public Properties
/// <summary>
/// The JSONPath for this item
/// </summary>
public string FullPath { get; set; }
/// <summary>
/// The type of project item
/// </summary>
public ProjectItemType Type { get; set; }
/// <summary>
/// Gets and sets the image name associated with project tree view headers.
/// </summary>
public string ImageName
{
get
{
switch (Type)
{
case ProjectItemType.Channel:
return "channel";
case ProjectItemType.Device:
return "device";
default:
return "blink";
}
}
}
/// <summary>
/// Gets the name of the item as a string
/// </summary>
public string Name => ProjectHelpers.GetPropertyValue(FullPath, "Name");
/// <summary>
/// Gets the associated driver as a string
/// </summary>
public string Driver => ProjectHelpers.GetPropertyValue(FullPath, "Driver");
/// <summary>
/// A list of all children contained inside this item
/// </summary>
public ObservableCollection<ProjectTreeItemViewModel> Children { get; set; }
/// <summary>
/// Indicates if this item can be expanded
/// </summary>
public bool CanExpand => (Type != ProjectItemType.Device);
/// <summary>
/// Indicates that the tree view item is selected, bound to the UI
/// </summary>
public bool IsSelected { get; set; }
/// <summary>
/// Indicates if the current item is expanded or not
/// </summary>
public bool IsExpanded
{
get {
return (Children?.Count(f => f != null) >= 1);
}
set {
//-- If the UI tells us to expand...
if (value == true)
//-- Find all children
Expand();
//-- If the UI tells us to close
else
this.ClearChildren();
}
}
#endregion
#region Commands
/// <summary>
/// The command to expand this item
/// </summary>
public ICommand ExpandCommand { get; set; }
/// <summary>
/// Command bound by left mouse click on tree view item
/// </summary>
public ICommand GetNodeDataCommand { get; set; }
#endregion
#region Public Methods
/// <summary>
/// Expands a tree view item
/// </summary>
public void Expand()
{
//-- return if we are either a device or already expanded
if (this.Type == ProjectItemType.Device || this.IsExpanded == true)
return;
//-- find all children
var children = ProjectHelpers.GetChildrenByName(FullPath, "Devices");
this.Children = new ObservableCollection<ProjectTreeItemViewModel>(
children.Select(c => new ProjectTreeItemViewModel(c.Path, ProjectHelpers.GetItemType(FullPath))));
}
/// <summary>
/// Clears all children of this node
/// </summary>
public void ClearChildren()
{
//-- Clear items
this.Children = new ObservableCollection<ProjectTreeItemViewModel>();
//-- Show the expand arrow if we are not a device
if (this.Type != ProjectItemType.Device)
this.Children.Add(null);
}
/// <summary>
/// Clears the children and expands it if it has children
/// </summary>
public void Reset()
{
this.ClearChildren();
if (this.Children?.Count > 0)
this.Expand();
}
#endregion
#region Public Methods
/// <summary>
/// Shows the view model data in the node context data grid
/// </summary>
public void GetNodeData()
{
switch (Type)
{
//-- get the devices associated with that channel
case ProjectItemType.Channel:
IoC.Application.UpdateDeviceDataContext(FullPath);
break;
//-- get the tags associated with that device
case ProjectItemType.Device:
IoC.Application.UpdateTagDataContext(FullPath);
break;
}
}
#endregion
}
这是我的树视图项目模板:
<Style x:Key="BaseTreeViewItemTemplate" TargetType="{x:Type TreeViewItem}">
<Setter Property="Panel.ZIndex" Value="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource TreeViewItemZIndexConverter}}" />
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="Padding" Value="1,2,2,2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Grid Name="ItemRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Name="Lines" Grid.Column="0" Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!-- L shape -->
<Border Grid.Row="0" Grid.Column="1" Name="TargetLine" BorderThickness="1 0 0 1" SnapsToDevicePixels="True" BorderBrush="Red"/>
<!-- line that follows a tree view item -->
<Border Name="LineToNextItem"
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TreeLineVisibilityConverter}}"
Grid.Row="1" Grid.Column="1" BorderThickness="1 0 0 0" SnapsToDevicePixels="True" BorderBrush="Blue"/>
</Grid>
<ToggleButton x:Name="Expander" Grid.Column="0" Grid.Row="0"
Style="{StaticResource ExpandCollapseToggleStyle}"
IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press"/>
<!-- selected border background -->
<Border Name="ContentBorder" Grid.Column="1" Grid.Row="0"
HorizontalAlignment="Left"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<ContentPresenter x:Name="ContentHeader" ContentSource="Header" MinWidth="20"/>
</Border>
<Grid Grid.Column="0" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border BorderThickness="1 0 0 0"
Name="TargetBorder"
Grid.Column="1"
SnapsToDevicePixels="True"
BorderBrush="Olive"
Visibility="{Binding ElementName=LineToNextItem, Path=Visibility}"
/>
</Grid>
<ItemsPresenter x:Name="ItemsHost" Grid.Column="1" Grid.Row="1" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="HasItems" Value="false">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
</Trigger>
<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="ContentHeader" Property="MinWidth" Value="75"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader" Value="False"/>
<Condition Property="Height" Value="Auto"/>
</MultiTrigger.Conditions>
<Setter TargetName="ContentHeader" Property="MinHeight" Value="19"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Foreground" Value="{StaticResource OffWhiteBaseBrush}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="True"/>
<Condition Property="IsSelectionActive" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="ContentBorder" Property="Background" Value="{StaticResource SelectedTreeViewItemColor}"/>
<Setter Property="Foreground" Value="White" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
我的自定义树视图控件
<UserControl ...>
<UserControl.Template>
<ControlTemplate TargetType="UserControl">
<StackPanel Background="Transparent"
Margin="8"
Orientation="Vertical"
VerticalAlignment="Top"
HorizontalAlignment="Left"
TextBlock.TextAlignment="Left">
<Image x:Name="Root"
ContextMenuOpening="OnContextMenuOpened"
Width="18" Height="18"
HorizontalAlignment="Left"
RenderOptions.BitmapScalingMode="HighQuality"
Margin="2.7 0 0 3"
Source="{Binding RootImageName, Converter={x:Static local:HeaderToImageConverter.Instance}}" />
<TreeView Name="ProjectTreeView"
Loaded="OnTreeViewLoaded"
SelectedItemChanged="OnTreeViewSelectedItemChanged"
ContextMenuOpening="OnContextMenuOpened"
BorderBrush="Transparent"
Background="Transparent"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
Style="{StaticResource ResourceKey=BaseTreeViewTemplate}"
ItemContainerStyle="{StaticResource ResourceKey=BaseTreeViewItemTemplate}"
ItemsSource="{Binding ApplicationViewModel.Channels, Source={x:Static local:ViewModelLocator.Instance}}">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="New Item" />
<MenuItem Header="Cut" />
<MenuItem Header="Copy" />
<MenuItem Header="Delete" />
<MenuItem Header="Diagnostics" />
<MenuItem Header="Properties" />
</ContextMenu>
</TreeView.ContextMenu>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">
<StackPanel Orientation="Horizontal" Margin="2">
<Image Width="15" Height="15" RenderOptions.BitmapScalingMode="HighQuality"
Margin="-1 0 0 0"
Source="{Binding Path=ImageName, Converter={x:Static local:HeaderToImageConverter.Instance}}" />
<TextBlock Margin="6,2,2,0" VerticalAlignment="Center" Text="{Binding Path=Name}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<ContentPresenter />
</StackPanel>
</ControlTemplate>
</UserControl.Template>
</UserControl>
树视图模板中连接线的可见性转换器
/// <summary>
/// Visibility converter for a connecting line inside the tree view UI
/// </summary>
public class TreeLineVisibilityConverter : BaseValueConverter<TreeLineVisibilityConverter>
{
public override object Convert(object value, Type targetType = null, object parameter = null, CultureInfo culture = null)
{
TreeViewItem item = (TreeViewItem)value;
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
bool isLastItem = (ic.ItemContainerGenerator.IndexFromContainer(item) == ic.Items.Count - 1);
return isLastItem ? Visibility.Hidden : Visibility.Visible;
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
此绑定存在问题:
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TreeLineVisibilityConverter}}"
您正在绑定到项目容器本身。该值永远不会改变,因此 Binding
仅在模板应用于容器时触发一次。
只要 ItemsSource
发生变化,您就应该绑定到也会发生变化的 属性。我认为最好的解决方案是将此逻辑移动到项目 and/or 转换器。
为此,我向数据模型 ProjectTreeItemViewModel
添加了一个 IsLast
属性,它必须在更改时引发 INotifyPropertyChanged.PropertyChanged
。
这个属性的初始默认值应该是false
.
边框可见性绑定到此 属性 使用您现有的,但已修改 TreeLineVisibilityConverter
。
转换器必须变成 IMultiValueConverter
,因为我们需要使用 MultiBinding
.
绑定到新的 ProjectTreeItemViewModel.IsLast
和项目本身。
每当向 TreeView
添加新项目时,都会加载其模板。这将触发 MultiBinding
,因此会触发 IMultiValueConverter
。转换器检查当前项目是否是最后一项。如果是这样,他会
将上一项ProjectTreeItemViewModel.IsLast
设置为false
,会重新触发
MultiBinding
为前一项显示行。
将当前 ProjectTreeItemViewModel.IsLast
设置为 true
。
- Return合适
Visibility
.
TreeLineVisibilityConverter.cs
public class TreeLineVisibilityConverter : IMultiValueConverter
{
public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
TreeViewItem item = (TreeViewItem) values[0];
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
int lastIndex = ic.Items.Count - 1;
bool isLastItem = (ic.ItemContainerGenerator.IndexFromContainer(item) == lastIndex);
if (isLastItem)
{
ResetIsLastOfPrevousItem(ic.Items.Cast<ProjectTreeItemViewModel>(), lastIndex);
(item.DataContext as ProjectTreeItemViewModel).IsLast = true;
}
return isLastItem
? Visibility.Hidden
: Visibility.Visible;
}
private void ConvertBack(IEnumerable<ProjectTreeItemViewModel> items, int lastIndex)
{
ProjectTreeItemViewModel previousItem = items.ElementAt(lastIndex - 1);
if (previousItem.IsLast && items.Count() > 1)
{
previousItem.IsLast = false;
}
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
ControlTemplate
共 TreeViewItem
<ControlTemplate TargetType="TreeViewItem">
...
<!-- line that follows a tree view item -->
<Border Name="LineToNextItem">
<Border.Visibility>
<MultiBinding Converter="{StaticResource TreeLineVisibilityConverter}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="IsLast" />
</MultiBinding>
</Border.Visibility>
</Border>
...
</ControlTemplate>
备注
出于性能原因,您应该考虑将 Parent
属性 添加到您的 ProjectTreeItemViewModel
。遍历模型树比遍历可视化树更有效。然后在您的 ControlTemplate
中,您只需将对 TemplatedParent
(TreeViewItem
) 的绑定替换为对 ControlTemplate
的 DataContext
的绑定,例如 {Binding}
(或 <Binding />
在 MultiBinding
的情况下),这将 return 当前 ProjectTreeItemViewModel
。从这里您可以通过 ProjectTreeItemViewModel.Parent
访问 ProjectTreeItemViewModel.Children
属性 来检查它是否是最后一个。这样你就不必使用 ItemContainerGenerator
也不必将 ItemsControl.Items
的项目转换为 IEnumerable<ProjectTreeItemViewModel>
.
MVVM 树视图示例
这是一个关于如何使用 MVVM 构建树的简单示例。此示例假装从文本文件创建数据树。
请参阅 ProjectTreeItem
class 了解如何使用递归遍历树,例如GetTreeRoot()
。
最后也是 TreeLineVisibilityConverter
的修订版,展示了如何使用 Parent
引用访问父集合(因此不需要 static
属性)。
ProjectTreeItem.cs
// The data view model of the tree items.
// Since this is the binding source of the TreeView,
// this class should implement INotifyPropertyChanged.
// This classes property setters are simplified.
public class ProjectTreeItem : INotifyPropertyChanged
{
/// <summary>
/// Default constructor
/// </summary>
public ProjectTreeItem(string data)
{
this.Data = data;
this.Parent = null;
this.Children = new ObservableCollection<ProjectTreeItem>();
}
// Traverse tree and expand subtree.
public ExpandChildren()
{
foreach (var child in this.Children)
{
child.IsExpanded = true;
child.ExpandChildren();
}
}
// Traverse complete tree and expand each item.
public ExpandTree()
{
// Get the root of the tree
ProjectTreeItem rootItem = GetTreeRoot(this);
foreach (var child in rootItem.Children)
{
child.IsExpanded = true;
child.ExpandChildren();
}
}
// Traverse the tree to the root using recursion.
private ProjectTreeItem GetTreeRoot(ProjectTreeItem treeItem)
{
// Check if item is the root
if (treeItem.Parent == null)
{
return treeItem;
}
return GetTreeRoot(treeItem.Parent);
}
public string Data { get; set; }
public bool IsExpanded { get; set; }
public ProjectTreeItem Parent { get; set; }
public ObservableCollection<ProjectTreeItem> Children { get; set; }
}
Repository.cs
// A model class in the sense of MVVM
public class Repository
{
public ProjectTreeItem ReadData()
{
var lines = File.ReadAllLines("/path/to/data");
// Create the tree structure from the file data
return CreateDataModel(lines);
}
private ProjectTreeItem CreateDataModel(string[] lines)
{
var rootItem = new ProjectTreeItem(string.Empty);
// Pretend each line contains tokens separated by a whitespace,
// then each line is a parent and the tokens its children.
// Just to show how to build the tree by setting Parent and Children.
foreach (string line in lines)
{
rootItem.Children.Add(CreateNode(line));
}
return rootItem;
}
private ProjectTreeItem CreateNode(string line)
{
var nodeItem = new ProjectTreeItem(line);
foreach (string token in line.Split(' '))
{
nodeItem.Children.Add(new ProjectTreeItem(token) {Parent = nodeItem});
}
return nodeItem;
}
}
DataController.cs
// Another model class in the sense of MVVM
public class DataController
{
public DataController()
{
// Create the model. Alternatively use constructor
this.Repository = new Repository();
}
public IEnumerable<ProjectTreeItem> GetData()
{
return this.Repository.ReadData().Children;
}
private Repository Repository { get; set; }
}
MainViewModel.cs
// The data view model of the tree items.
// Since this is a binding source of the view,
// this class should implement INotifyPropertyChanged.
// This classes property setters are simplified.
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
// Create the model. Alternatively use constructor injection.
this.DataController = new DataController();
Initialize();
}
private void Initialize()
{
IEnumerable<ProjectTreeItem> treeData = this.DataController.GetData();
this.TreeData = new ObservableCollection<ProjectTreeItem>(treeData);
}
public ObservableCollection<ProjectTreeItem> TreeData { get; set; }
private DataController DataController { get; set; }
}
TreeLineVisibilityConverter.cs
public class TreeLineVisibilityConverter : IMultiValueConverter
{
public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
ProjectTreeItem item = values[0] as ProjectTreeItem;
// If current item is root return
if (item.Parent == null)
{
return Binding.DoNothing;
}
ProjectTreeItem parent = item?.Parent ?? item;
int lastIndex = item.Parent.Chilidren.Count - 1;
bool isLastItem = item.Parent.Chilidren.IndexOf(item) == lastIndex);
if (isLastItem)
{
ResetIsLastOfPrevousItem(item.Parent.Chilidren, lastIndex);
item.IsLast = true;
}
return isLastItem
? Visibility.Hidden
: Visibility.Visible;
}
private void ConvertBack(IEnumerable<ProjectTreeItem> items, int lastIndex)
{
ProjectTreeItem previousItem = items.ElementAt(lastIndex - 1);
if (previousItem.IsLast && items.Count() > 1)
{
previousItem.IsLast = false;
}
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
UserControl.xaml
<UserControl>
<UserControl.DataContext>
<MainViewModel />
<UserControl.DataContext>
<UserControl.Resources>
<ControlTemplate TargetType="TreeViewItem">
...
<!-- line that follows a tree view item -->
<Border Name="LineToNextItem">
<Border.Visibility>
<MultiBinding Converter="{StaticResource TreeLineVisibilityConverter}">
<Binding />
<Binding Path="IsLast" />
</MultiBinding>
</Border.Visibility>
</Border>
...
</ControlTemplate>
<UserControl.Resources>
<TreeView ItemsSource="{Binding TreeData}" />
</UserControl>
在此感谢@BionicCode 的帮助,意义重大。我想分享我的视图模型遍历代替可视化树遍历的实现。我最终没有创建一个字段来引用 ProjectTreeItemViewModel class 中的父容器,而是创建了一个 ParentIndex 和一个 ChildIndex,它们允许我通过引用 FullPath 属性 来快速访问我需要的项目,这只是 json 内容的 JSONPath。老实说,我不太确定您打算如何在 class 中包含对父容器的引用,但希望看到您建议的实现。再次感谢@BionicCode,周末愉快!
这是我现在的转换器:
/// <summary>
/// Visibility converter for the connecting lines on the tree view UI
/// </summary>
public class ConnectingLineVisibilityConverter : IMultiValueConverter
{
/// <summary>
/// Returns the proper visibility according to location on the tree view UI
/// </summary>
public object Convert(object[] values, Type targetType = null, object parameter = null, CultureInfo culture = null)
{
ProjectTreeItemViewModel viewModel = (ProjectTreeItemViewModel)values[0];
//-- collection context by default is the channels
var collection = IoC.Application.Channels;
int currentIndex = viewModel.ParentIndex;
if (viewModel.Type == ProjectItemType.Device) {
//-- change the collection context to the children of this channel
collection = collection[currentIndex].Children;
currentIndex = viewModel.ChildIndex;
}
int lastIndex = collection.Count - 1;
bool isLastItem = (currentIndex == lastIndex);
//-- is it the last of it's branch?
if (isLastItem) {
ResetPreviousSibling(collection, lastIndex);
viewModel.IsLast = true;
}
return isLastItem ? Visibility.Hidden : Visibility.Visible;
}
/// <summary>
/// Resets the previous sibling IsLast flag once a new item is added to the collection
/// </summary>
/// <param name="collection">The collection to search</param>
/// <param name="lastIndex">The index of the previous sibling</param>
private void ResetPreviousSibling(ObservableCollection<ProjectTreeItemViewModel> collection, int lastIndex)
{
//-- there's only one item in the collection
if (lastIndex == 0)
return;
//-- get the previous sibling and reset it's IsLast flag, if necessary
ProjectTreeItemViewModel previousSibling = collection[lastIndex - 1];
if (previousSibling.IsLast)
previousSibling.IsLast = false;
}
public object[] ConvertBack(object value, Type[] targetTypes = null, object parameter = null, CultureInfo culture = null)
{
throw new NotImplementedException();
}
}
那么,绑定就变成了...
<!-- connecting line to the next item -->
<Border Name="LineToNextItem" Grid.Row="1" Grid.Column="1" BorderThickness="1 0 0 0" SnapsToDevicePixels="True" BorderBrush="Blue">
<Border.Visibility>
<MultiBinding Converter="{StaticResource ConnectingLineVisibilityConverter}">
<Binding />
<Binding Path="IsLast" />
</MultiBinding>
</Border.Visibility>
</Border>
我构建了一个绑定到可观察集合的树视图,并在每个树视图项之间建立了连接线。正在使用的视图模型实现了 INotifyPropertyChanged,我正在使用 PropertyChanged.Fody 进行编织。树视图绑定到集合并且正在更新,除了一件事。当我在运行时向列表添加新项目时,UI 似乎没有正确更新。我已经尝试了所有我能找到的在网上搜索如何强制更新 UI 的方法,而无需在添加根项时发送命令来重建整个树,这确实有效,但是必须有另一种方式我没有找到。
我正在使用 Ninject 进行依赖注入。
我会把所有的代码放在我的问题下面,以供参考。同样,所有这些都运行良好,直到在运行时将一个项目添加到集合中。一旦添加到集合中,该项目就会添加并在树视图中可见,但最后一行转换器不会正确更新所有图形。
考虑下图:
添加项目后,现在成为倒数第二个节点,他的连接线可见性不会更新,他仍然认为他是分支上的最后一个。我已经尝试了我能找到的所有类型的 UI 刷新方法,但没有任何效果。我在这里遗漏了一些东西,但我对 WPF 还很陌生。任何人都可以提供的任何建议将不胜感激。谢谢!
这是我最初构建树视图的方式,效果很好:
ProjectHelpers.JsonObject = JObject.Parse(File.ReadAllText(ProjectPath.BaseDataFullPath));
//-- Get the channels, which are the top level tree elements
var children = ProjectHelpers.GetChannels();
//-- add the channels to the application channel collection
IoC.Application.Channels = new ObservableCollection<ProjectTreeItemViewModel>();
foreach(var c in children)
IoC.Application.Channels.Add(new ProjectTreeItemViewModel(c.Path, ProjectItemType.Channel));
其中包含 class:
/// <summary>
/// The view model for the main project tree view
/// </summary>
public class ProjectTreeViewModel : BaseViewModel
{
/// <summary>
/// Name of the image displayed above the tree view UI
/// </summary>
public string RootImageName => "blink";
/// <summary>
/// Default constructor
/// </summary>
public ProjectTreeViewModel()
{
BuildProjectTree();
}
#region Handlers : Building project data tree
/// <summary>
/// Builds the entire project tree
/// </summary>
public void BuildProjectTree()
{
ProjectHelpers.JsonObject = JObject.Parse(File.ReadAllText(ProjectPath.BaseDataFullPath));
//-- Get the channels, which are the top level tree elements
var children = ProjectHelpers.GetChannels();
//-- add the channels to the application channel collection
IoC.Application.Channels = new ObservableCollection<ProjectTreeItemViewModel>();
foreach(var c in children)
IoC.Application.Channels.Add(new ProjectTreeItemViewModel(c.Path, ProjectItemType.Channel));
}
#endregion
}
添加到可观察集合的项目的视图模型
/// <summary>
/// The view model that represents an item within the tree view
/// </summary>
public class ProjectTreeItemViewModel : BaseViewModel
{
/// <summary>
/// Default constructor
/// </summary>
/// <param name="path">The JSONPath for the item</param>
/// <param name="type">The type of project item type</param>
public ProjectTreeItemViewModel(string path = "", ProjectItemType type = ProjectItemType.Channel)
{
//-- Create commands
ExpandCommand = new RelayCommand(Expand);
GetNodeDataCommand = new RelayCommand(GetNodeData);
FullPath = path;
Type = type;
//-- Setup the children as needed
ClearChildren();
}
#region Public Properties
/// <summary>
/// The JSONPath for this item
/// </summary>
public string FullPath { get; set; }
/// <summary>
/// The type of project item
/// </summary>
public ProjectItemType Type { get; set; }
/// <summary>
/// Gets and sets the image name associated with project tree view headers.
/// </summary>
public string ImageName
{
get
{
switch (Type)
{
case ProjectItemType.Channel:
return "channel";
case ProjectItemType.Device:
return "device";
default:
return "blink";
}
}
}
/// <summary>
/// Gets the name of the item as a string
/// </summary>
public string Name => ProjectHelpers.GetPropertyValue(FullPath, "Name");
/// <summary>
/// Gets the associated driver as a string
/// </summary>
public string Driver => ProjectHelpers.GetPropertyValue(FullPath, "Driver");
/// <summary>
/// A list of all children contained inside this item
/// </summary>
public ObservableCollection<ProjectTreeItemViewModel> Children { get; set; }
/// <summary>
/// Indicates if this item can be expanded
/// </summary>
public bool CanExpand => (Type != ProjectItemType.Device);
/// <summary>
/// Indicates that the tree view item is selected, bound to the UI
/// </summary>
public bool IsSelected { get; set; }
/// <summary>
/// Indicates if the current item is expanded or not
/// </summary>
public bool IsExpanded
{
get {
return (Children?.Count(f => f != null) >= 1);
}
set {
//-- If the UI tells us to expand...
if (value == true)
//-- Find all children
Expand();
//-- If the UI tells us to close
else
this.ClearChildren();
}
}
#endregion
#region Commands
/// <summary>
/// The command to expand this item
/// </summary>
public ICommand ExpandCommand { get; set; }
/// <summary>
/// Command bound by left mouse click on tree view item
/// </summary>
public ICommand GetNodeDataCommand { get; set; }
#endregion
#region Public Methods
/// <summary>
/// Expands a tree view item
/// </summary>
public void Expand()
{
//-- return if we are either a device or already expanded
if (this.Type == ProjectItemType.Device || this.IsExpanded == true)
return;
//-- find all children
var children = ProjectHelpers.GetChildrenByName(FullPath, "Devices");
this.Children = new ObservableCollection<ProjectTreeItemViewModel>(
children.Select(c => new ProjectTreeItemViewModel(c.Path, ProjectHelpers.GetItemType(FullPath))));
}
/// <summary>
/// Clears all children of this node
/// </summary>
public void ClearChildren()
{
//-- Clear items
this.Children = new ObservableCollection<ProjectTreeItemViewModel>();
//-- Show the expand arrow if we are not a device
if (this.Type != ProjectItemType.Device)
this.Children.Add(null);
}
/// <summary>
/// Clears the children and expands it if it has children
/// </summary>
public void Reset()
{
this.ClearChildren();
if (this.Children?.Count > 0)
this.Expand();
}
#endregion
#region Public Methods
/// <summary>
/// Shows the view model data in the node context data grid
/// </summary>
public void GetNodeData()
{
switch (Type)
{
//-- get the devices associated with that channel
case ProjectItemType.Channel:
IoC.Application.UpdateDeviceDataContext(FullPath);
break;
//-- get the tags associated with that device
case ProjectItemType.Device:
IoC.Application.UpdateTagDataContext(FullPath);
break;
}
}
#endregion
}
这是我的树视图项目模板:
<Style x:Key="BaseTreeViewItemTemplate" TargetType="{x:Type TreeViewItem}">
<Setter Property="Panel.ZIndex" Value="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource TreeViewItemZIndexConverter}}" />
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="Padding" Value="1,2,2,2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Grid Name="ItemRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Name="Lines" Grid.Column="0" Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!-- L shape -->
<Border Grid.Row="0" Grid.Column="1" Name="TargetLine" BorderThickness="1 0 0 1" SnapsToDevicePixels="True" BorderBrush="Red"/>
<!-- line that follows a tree view item -->
<Border Name="LineToNextItem"
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TreeLineVisibilityConverter}}"
Grid.Row="1" Grid.Column="1" BorderThickness="1 0 0 0" SnapsToDevicePixels="True" BorderBrush="Blue"/>
</Grid>
<ToggleButton x:Name="Expander" Grid.Column="0" Grid.Row="0"
Style="{StaticResource ExpandCollapseToggleStyle}"
IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press"/>
<!-- selected border background -->
<Border Name="ContentBorder" Grid.Column="1" Grid.Row="0"
HorizontalAlignment="Left"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<ContentPresenter x:Name="ContentHeader" ContentSource="Header" MinWidth="20"/>
</Border>
<Grid Grid.Column="0" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border BorderThickness="1 0 0 0"
Name="TargetBorder"
Grid.Column="1"
SnapsToDevicePixels="True"
BorderBrush="Olive"
Visibility="{Binding ElementName=LineToNextItem, Path=Visibility}"
/>
</Grid>
<ItemsPresenter x:Name="ItemsHost" Grid.Column="1" Grid.Row="1" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="HasItems" Value="false">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
</Trigger>
<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="ContentHeader" Property="MinWidth" Value="75"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader" Value="False"/>
<Condition Property="Height" Value="Auto"/>
</MultiTrigger.Conditions>
<Setter TargetName="ContentHeader" Property="MinHeight" Value="19"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Foreground" Value="{StaticResource OffWhiteBaseBrush}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="True"/>
<Condition Property="IsSelectionActive" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="ContentBorder" Property="Background" Value="{StaticResource SelectedTreeViewItemColor}"/>
<Setter Property="Foreground" Value="White" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
我的自定义树视图控件
<UserControl ...>
<UserControl.Template>
<ControlTemplate TargetType="UserControl">
<StackPanel Background="Transparent"
Margin="8"
Orientation="Vertical"
VerticalAlignment="Top"
HorizontalAlignment="Left"
TextBlock.TextAlignment="Left">
<Image x:Name="Root"
ContextMenuOpening="OnContextMenuOpened"
Width="18" Height="18"
HorizontalAlignment="Left"
RenderOptions.BitmapScalingMode="HighQuality"
Margin="2.7 0 0 3"
Source="{Binding RootImageName, Converter={x:Static local:HeaderToImageConverter.Instance}}" />
<TreeView Name="ProjectTreeView"
Loaded="OnTreeViewLoaded"
SelectedItemChanged="OnTreeViewSelectedItemChanged"
ContextMenuOpening="OnContextMenuOpened"
BorderBrush="Transparent"
Background="Transparent"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
Style="{StaticResource ResourceKey=BaseTreeViewTemplate}"
ItemContainerStyle="{StaticResource ResourceKey=BaseTreeViewItemTemplate}"
ItemsSource="{Binding ApplicationViewModel.Channels, Source={x:Static local:ViewModelLocator.Instance}}">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="New Item" />
<MenuItem Header="Cut" />
<MenuItem Header="Copy" />
<MenuItem Header="Delete" />
<MenuItem Header="Diagnostics" />
<MenuItem Header="Properties" />
</ContextMenu>
</TreeView.ContextMenu>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">
<StackPanel Orientation="Horizontal" Margin="2">
<Image Width="15" Height="15" RenderOptions.BitmapScalingMode="HighQuality"
Margin="-1 0 0 0"
Source="{Binding Path=ImageName, Converter={x:Static local:HeaderToImageConverter.Instance}}" />
<TextBlock Margin="6,2,2,0" VerticalAlignment="Center" Text="{Binding Path=Name}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<ContentPresenter />
</StackPanel>
</ControlTemplate>
</UserControl.Template>
</UserControl>
树视图模板中连接线的可见性转换器
/// <summary>
/// Visibility converter for a connecting line inside the tree view UI
/// </summary>
public class TreeLineVisibilityConverter : BaseValueConverter<TreeLineVisibilityConverter>
{
public override object Convert(object value, Type targetType = null, object parameter = null, CultureInfo culture = null)
{
TreeViewItem item = (TreeViewItem)value;
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
bool isLastItem = (ic.ItemContainerGenerator.IndexFromContainer(item) == ic.Items.Count - 1);
return isLastItem ? Visibility.Hidden : Visibility.Visible;
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
此绑定存在问题:
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TreeLineVisibilityConverter}}"
您正在绑定到项目容器本身。该值永远不会改变,因此 Binding
仅在模板应用于容器时触发一次。
只要 ItemsSource
发生变化,您就应该绑定到也会发生变化的 属性。我认为最好的解决方案是将此逻辑移动到项目 and/or 转换器。
为此,我向数据模型 ProjectTreeItemViewModel
添加了一个 IsLast
属性,它必须在更改时引发 INotifyPropertyChanged.PropertyChanged
。
这个属性的初始默认值应该是false
.
边框可见性绑定到此 属性 使用您现有的,但已修改 TreeLineVisibilityConverter
。
转换器必须变成 IMultiValueConverter
,因为我们需要使用 MultiBinding
.
ProjectTreeItemViewModel.IsLast
和项目本身。
每当向 TreeView
添加新项目时,都会加载其模板。这将触发 MultiBinding
,因此会触发 IMultiValueConverter
。转换器检查当前项目是否是最后一项。如果是这样,他会
将上一项
ProjectTreeItemViewModel.IsLast
设置为false
,会重新触发MultiBinding
为前一项显示行。将当前
ProjectTreeItemViewModel.IsLast
设置为true
。- Return合适
Visibility
.
TreeLineVisibilityConverter.cs
public class TreeLineVisibilityConverter : IMultiValueConverter
{
public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
TreeViewItem item = (TreeViewItem) values[0];
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
int lastIndex = ic.Items.Count - 1;
bool isLastItem = (ic.ItemContainerGenerator.IndexFromContainer(item) == lastIndex);
if (isLastItem)
{
ResetIsLastOfPrevousItem(ic.Items.Cast<ProjectTreeItemViewModel>(), lastIndex);
(item.DataContext as ProjectTreeItemViewModel).IsLast = true;
}
return isLastItem
? Visibility.Hidden
: Visibility.Visible;
}
private void ConvertBack(IEnumerable<ProjectTreeItemViewModel> items, int lastIndex)
{
ProjectTreeItemViewModel previousItem = items.ElementAt(lastIndex - 1);
if (previousItem.IsLast && items.Count() > 1)
{
previousItem.IsLast = false;
}
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
ControlTemplate
共 TreeViewItem
<ControlTemplate TargetType="TreeViewItem">
...
<!-- line that follows a tree view item -->
<Border Name="LineToNextItem">
<Border.Visibility>
<MultiBinding Converter="{StaticResource TreeLineVisibilityConverter}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="IsLast" />
</MultiBinding>
</Border.Visibility>
</Border>
...
</ControlTemplate>
备注
出于性能原因,您应该考虑将 Parent
属性 添加到您的 ProjectTreeItemViewModel
。遍历模型树比遍历可视化树更有效。然后在您的 ControlTemplate
中,您只需将对 TemplatedParent
(TreeViewItem
) 的绑定替换为对 ControlTemplate
的 DataContext
的绑定,例如 {Binding}
(或 <Binding />
在 MultiBinding
的情况下),这将 return 当前 ProjectTreeItemViewModel
。从这里您可以通过 ProjectTreeItemViewModel.Parent
访问 ProjectTreeItemViewModel.Children
属性 来检查它是否是最后一个。这样你就不必使用 ItemContainerGenerator
也不必将 ItemsControl.Items
的项目转换为 IEnumerable<ProjectTreeItemViewModel>
.
MVVM 树视图示例
这是一个关于如何使用 MVVM 构建树的简单示例。此示例假装从文本文件创建数据树。
请参阅 ProjectTreeItem
class 了解如何使用递归遍历树,例如GetTreeRoot()
。
最后也是 TreeLineVisibilityConverter
的修订版,展示了如何使用 Parent
引用访问父集合(因此不需要 static
属性)。
ProjectTreeItem.cs
// The data view model of the tree items.
// Since this is the binding source of the TreeView,
// this class should implement INotifyPropertyChanged.
// This classes property setters are simplified.
public class ProjectTreeItem : INotifyPropertyChanged
{
/// <summary>
/// Default constructor
/// </summary>
public ProjectTreeItem(string data)
{
this.Data = data;
this.Parent = null;
this.Children = new ObservableCollection<ProjectTreeItem>();
}
// Traverse tree and expand subtree.
public ExpandChildren()
{
foreach (var child in this.Children)
{
child.IsExpanded = true;
child.ExpandChildren();
}
}
// Traverse complete tree and expand each item.
public ExpandTree()
{
// Get the root of the tree
ProjectTreeItem rootItem = GetTreeRoot(this);
foreach (var child in rootItem.Children)
{
child.IsExpanded = true;
child.ExpandChildren();
}
}
// Traverse the tree to the root using recursion.
private ProjectTreeItem GetTreeRoot(ProjectTreeItem treeItem)
{
// Check if item is the root
if (treeItem.Parent == null)
{
return treeItem;
}
return GetTreeRoot(treeItem.Parent);
}
public string Data { get; set; }
public bool IsExpanded { get; set; }
public ProjectTreeItem Parent { get; set; }
public ObservableCollection<ProjectTreeItem> Children { get; set; }
}
Repository.cs
// A model class in the sense of MVVM
public class Repository
{
public ProjectTreeItem ReadData()
{
var lines = File.ReadAllLines("/path/to/data");
// Create the tree structure from the file data
return CreateDataModel(lines);
}
private ProjectTreeItem CreateDataModel(string[] lines)
{
var rootItem = new ProjectTreeItem(string.Empty);
// Pretend each line contains tokens separated by a whitespace,
// then each line is a parent and the tokens its children.
// Just to show how to build the tree by setting Parent and Children.
foreach (string line in lines)
{
rootItem.Children.Add(CreateNode(line));
}
return rootItem;
}
private ProjectTreeItem CreateNode(string line)
{
var nodeItem = new ProjectTreeItem(line);
foreach (string token in line.Split(' '))
{
nodeItem.Children.Add(new ProjectTreeItem(token) {Parent = nodeItem});
}
return nodeItem;
}
}
DataController.cs
// Another model class in the sense of MVVM
public class DataController
{
public DataController()
{
// Create the model. Alternatively use constructor
this.Repository = new Repository();
}
public IEnumerable<ProjectTreeItem> GetData()
{
return this.Repository.ReadData().Children;
}
private Repository Repository { get; set; }
}
MainViewModel.cs
// The data view model of the tree items.
// Since this is a binding source of the view,
// this class should implement INotifyPropertyChanged.
// This classes property setters are simplified.
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
// Create the model. Alternatively use constructor injection.
this.DataController = new DataController();
Initialize();
}
private void Initialize()
{
IEnumerable<ProjectTreeItem> treeData = this.DataController.GetData();
this.TreeData = new ObservableCollection<ProjectTreeItem>(treeData);
}
public ObservableCollection<ProjectTreeItem> TreeData { get; set; }
private DataController DataController { get; set; }
}
TreeLineVisibilityConverter.cs
public class TreeLineVisibilityConverter : IMultiValueConverter
{
public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
ProjectTreeItem item = values[0] as ProjectTreeItem;
// If current item is root return
if (item.Parent == null)
{
return Binding.DoNothing;
}
ProjectTreeItem parent = item?.Parent ?? item;
int lastIndex = item.Parent.Chilidren.Count - 1;
bool isLastItem = item.Parent.Chilidren.IndexOf(item) == lastIndex);
if (isLastItem)
{
ResetIsLastOfPrevousItem(item.Parent.Chilidren, lastIndex);
item.IsLast = true;
}
return isLastItem
? Visibility.Hidden
: Visibility.Visible;
}
private void ConvertBack(IEnumerable<ProjectTreeItem> items, int lastIndex)
{
ProjectTreeItem previousItem = items.ElementAt(lastIndex - 1);
if (previousItem.IsLast && items.Count() > 1)
{
previousItem.IsLast = false;
}
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
UserControl.xaml
<UserControl>
<UserControl.DataContext>
<MainViewModel />
<UserControl.DataContext>
<UserControl.Resources>
<ControlTemplate TargetType="TreeViewItem">
...
<!-- line that follows a tree view item -->
<Border Name="LineToNextItem">
<Border.Visibility>
<MultiBinding Converter="{StaticResource TreeLineVisibilityConverter}">
<Binding />
<Binding Path="IsLast" />
</MultiBinding>
</Border.Visibility>
</Border>
...
</ControlTemplate>
<UserControl.Resources>
<TreeView ItemsSource="{Binding TreeData}" />
</UserControl>
在此感谢@BionicCode 的帮助,意义重大。我想分享我的视图模型遍历代替可视化树遍历的实现。我最终没有创建一个字段来引用 ProjectTreeItemViewModel class 中的父容器,而是创建了一个 ParentIndex 和一个 ChildIndex,它们允许我通过引用 FullPath 属性 来快速访问我需要的项目,这只是 json 内容的 JSONPath。老实说,我不太确定您打算如何在 class 中包含对父容器的引用,但希望看到您建议的实现。再次感谢@BionicCode,周末愉快!
这是我现在的转换器:
/// <summary>
/// Visibility converter for the connecting lines on the tree view UI
/// </summary>
public class ConnectingLineVisibilityConverter : IMultiValueConverter
{
/// <summary>
/// Returns the proper visibility according to location on the tree view UI
/// </summary>
public object Convert(object[] values, Type targetType = null, object parameter = null, CultureInfo culture = null)
{
ProjectTreeItemViewModel viewModel = (ProjectTreeItemViewModel)values[0];
//-- collection context by default is the channels
var collection = IoC.Application.Channels;
int currentIndex = viewModel.ParentIndex;
if (viewModel.Type == ProjectItemType.Device) {
//-- change the collection context to the children of this channel
collection = collection[currentIndex].Children;
currentIndex = viewModel.ChildIndex;
}
int lastIndex = collection.Count - 1;
bool isLastItem = (currentIndex == lastIndex);
//-- is it the last of it's branch?
if (isLastItem) {
ResetPreviousSibling(collection, lastIndex);
viewModel.IsLast = true;
}
return isLastItem ? Visibility.Hidden : Visibility.Visible;
}
/// <summary>
/// Resets the previous sibling IsLast flag once a new item is added to the collection
/// </summary>
/// <param name="collection">The collection to search</param>
/// <param name="lastIndex">The index of the previous sibling</param>
private void ResetPreviousSibling(ObservableCollection<ProjectTreeItemViewModel> collection, int lastIndex)
{
//-- there's only one item in the collection
if (lastIndex == 0)
return;
//-- get the previous sibling and reset it's IsLast flag, if necessary
ProjectTreeItemViewModel previousSibling = collection[lastIndex - 1];
if (previousSibling.IsLast)
previousSibling.IsLast = false;
}
public object[] ConvertBack(object value, Type[] targetTypes = null, object parameter = null, CultureInfo culture = null)
{
throw new NotImplementedException();
}
}
那么,绑定就变成了...
<!-- connecting line to the next item -->
<Border Name="LineToNextItem" Grid.Row="1" Grid.Column="1" BorderThickness="1 0 0 0" SnapsToDevicePixels="True" BorderBrush="Blue">
<Border.Visibility>
<MultiBinding Converter="{StaticResource ConnectingLineVisibilityConverter}">
<Binding />
<Binding Path="IsLast" />
</MultiBinding>
</Border.Visibility>
</Border>