c# - 将项目添加到绑定的可观察集合时,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();
}
}
解决方案
由于此绑定而存在问题:
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
. 转换器检查当前项目是否是最后一个项目。如果是这样,他会
将上一项设置
ProjectTreeItemViewModel.IsLast
为false
,这将重新触发MultiBinding
上一项以显示该行。将电流设置
ProjectTreeItemViewModel.IsLast
为true
。- 返回相应的
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
) 与绑定到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>
推荐阅读
- python - 使用希腊字母作为变量名
- reactjs - 尝试代理请求时发生 HPM 错误
- python-3.x - 不同我层 ResNet50 和原始 ResNet50 的名称
- amazon-ec2 - AWS Batch 自定义 AMI 是否需要默认用户名?
- javascript - Array.map 不适用于 componentDidMount 中的异步状态更新 - React
- javascript - (Angular Ionic)如何使用 chartjs-plugin-streaming 流式传输我的自定义数据?
- python - 对名称和浮动的字典进行排序
- dictionary - 为什么字典复杂度低?
- mysql - 如何在 MySQL 中的 CSV 导入中将 int 转换为时间?
- amazon-dynamodb - 使用 dynamodb 在嵌套数组值上创建索引