[WPF树视图可见性转换器在将项目添加到绑定的可观察集合时不更新

问题描述 投票:0回答:2

我已经构建了绑定到可观察集合的树视图,并通过每个树视图项之间的连接线来构建它。正在使用的视图模型实现INotifyPropertyChanged,并且我正在使用PropertyChanged.Fody进行编织。树视图绑定到集合,并且正在更新一件事。当我在运行时将新项目添加到列表中时,UI似乎无法正确更新。我已经在阳光下进行了所有尝试,发现我可以在网上搜索如何强制对UI进行更新,而无需在添加根项目时发送命令来重建整个树,但这确实有效,但是必须我找不到的另一种方式。

我正在使用Ninject进行依赖项注入。

我将所有代码放在我的问题下方,以供参考。同样,所有这些工作都很好,直到在运行时将一个项目添加到集合中。一旦添加到集合中,该项目即被添加并在树视图中可见,但是最后一个线转换器不能正确更新所有图形。

考虑下图:

Tree view after a root level item is added

一旦添加了项目,现在变成倒数第二的节点,其连接线的可见性不会更新,并且他仍然认为自己是分支中的最后一个节点。我尝试了所有类型的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# wpf data-binding treeview treeviewitem
2个回答
0
投票

由于此绑定而存在问题:

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

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

[ItemsSource更改时,您也应绑定到也会更改的属性。我认为最好的解决方案是将这种逻辑转移到项目和/或转换器的解决方案。

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

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

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

[将新项目添加到TreeView时,将加载其模板。这将触发MultiBinding,因此会触发IMultiValueConverter。转换器检查当前项目是否为最后一项。如果是这样,他将

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

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

  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中,您只需将与TemplatedParentTreeViewItem)的绑定替换为与DataContextControlTemplate的绑定,例如{Binding}(如果是[C0,则为<Binding /> ]),它将返回当前的MultiBinding。在这里,您可以通过ProjectTreeItemViewModel访问ProjectTreeItemViewModel.Children属性来检查它是否是最后一个。这样,您就不必使用ProjectTreeItemViewModel.Parent,也不必将ItemContainerGenerator的项目强制转换为ItemsControl.Items


0
投票

感谢@BionicCode在这里提供帮助,意义非凡。我想分享我的视图模型遍历的实现,而不是可视树遍历。我最终没有在ProjectTreeItemViewModel类中创建一个字段来引用父容器,而是创建了ParentIndex和ChildIndex,使我可以通过引用FullPath属性(这只是JSON内容的JSONPath)来快速访问所需的项目。 。老实说,我不太确定您打算在类中包含对父容器的引用,但是希望看到您建议的实现。再次感谢@BionicCode,祝您周末愉快!

现在是我的转换器:

IEnumerable<ProjectTreeItemViewModel>

然后,绑定变为...


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

© www.soinside.com 2019 - 2024. All rights reserved.