首页 > 解决方案 > 将项目添加到绑定的可观察集合时,WPF 树视图可见性转换器不会更新

问题描述

我已经构建了一个绑定到可观察集合的树视图,并使用每个树视图项之间的连接线构建了它。正在使用的视图模型实现了 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));

包含在此类中:


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

标签: c#wpfdata-bindingtreeviewtreeviewitem

解决方案


由于此绑定而存在问题:

Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TreeLineVisibilityConverter}}"

您正在绑定到项目容器本身。此值永远不会更改,因此Binding仅在将模板应用于容器时触发一次。

每当发生更改时,您都应该绑定到也会更改的属性ItemsSource。我认为最好的解决方案是将这个逻辑移到项目和/或转换器上。

为此,我IsLast向数据模型添加了一个属性,该属性ProjectTreeItemViewModel必须在更改时引发INotifyPropertyChanged.PropertyChanged
此属性的初始默认值应为false

边框可见性使用您现有的但已修改的绑定到此属性TreeLineVisibilityConverter

转换器必须转换为 a IMultiValueConverter,因为我们需要ProjectTreeItemViewModel.IsLast使用 a 绑定到 new 和项目本身MultiBinding

每当向 中添加新项目时TreeView,都会加载其模板。这将触发MultiBinding并因此触发IMultiValueConverter. 转换器检查当前项目是否是最后一个项目。如果是这样,他会

  1. 将上一项设置ProjectTreeItemViewModel.IsLastfalse,这将重新触发MultiBinding上一项以显示该行。

  2. 将电流设置ProjectTreeItemViewModel.IsLasttrue

  3. 返回相应的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) 与绑定到DataContext例如ControlTemplate{Binding}(或<Binding />在的情况下MultiBinding),这将返回当前ProjectTreeItemViewModel。从这里您可以ProjectTreeItemViewModel.Children通过ProjectTreeItemViewModel.Parent. 这样您就不必使用 theItemContainerGenerator也不必将 to 的项目ItemsControl.Items转换为IEnumerable<ProjectTreeItemViewModel>



MVVM 树视图示例

这是一个关于如何使用 MVVM 构建树的简单示例。此示例假装从文本文件创建数据树。
请参阅ProjectTreeItem课程以了解如何使用递归遍历树,例如GetTreeRoot()

最后还有一个修订版,TreeLineVisibilityConverter展示了如何使用Parent引用访问父集合(因此不需要任何static属性)。

项目树项.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; }
}

存储库.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;
  }
}

数据控制器.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; }
}

主视图模型.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();
  }
}

用户控件.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>

推荐阅读