Проблема существует из-за этой привязки:
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TreeLineVisibilityConverter}}"
Вы привязываетесь к самому контейнеру элемента. Это значение никогда не изменяется, поэтому Binding
срабатывает только один раз, когда шаблон применяется к контейнеру.
При каждом изменении ItemsSource
следует также связать свойство, которое также изменяется. Я думаю, что лучшее решение - это то, что перемещает эту логику c к элементам и / или конвертеру.
Для этой цели я добавил свойство 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}
(или <Binding />
в случае MultiBinding
), который вернет текущее значение ProjectTreeItemViewModel
. Отсюда вы можете проверить, является ли он последним, обратившись к свойству ProjectTreeItemViewModel.Children
через ProjectTreeItemViewModel.Parent
. Таким образом, вам не нужно использовать ItemContainerGenerator
и не нужно приводить элементы ItemsControl.Items
к IEnumerable<ProjectTreeItemViewModel>
.
Пример представления дерева MVVM
Это простой пример того, как построить дерево с помощью MVVM. В этом примере делается вид, что создается дерево данных из текстового файла.
См. Класс ProjectTreeItem
, чтобы узнать, как пройти по дереву с помощью рекурсии, например, 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>