我已经构建了绑定到可观察集合的树视图,并通过每个树视图项之间的连接线来构建它。正在使用的视图模型实现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
绑定到此属性。
必须将转换器转换为IMultiValueConverter
,因为我们需要使用ProjectTreeItemViewModel.IsLast
绑定到新的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}
(如果是[C0,则为<Binding />
]),它将返回当前的MultiBinding
。在这里,您可以通过ProjectTreeItemViewModel
访问ProjectTreeItemViewModel.Children
属性来检查它是否是最后一个。这样,您就不必使用ProjectTreeItemViewModel.Parent
,也不必将ItemContainerGenerator
的项目强制转换为ItemsControl.Items
。
感谢@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();
}
}