当项目添加到绑定的 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。转换器检查当前项目是否是最后一项。如果是这样,他会

  1. 将上一项ProjectTreeItemViewModel.IsLast设置为false,会重新触发 MultiBinding 为前一项显示行。

  2. 将当前 ProjectTreeItemViewModel.IsLast 设置为 true

  3. 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();
  }
}

ControlTemplateTreeViewItem

<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) 的绑定替换为对 ControlTemplateDataContext 的绑定,例如 {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>