I'm trying to find code or a pre-packaged control that takes an object graph and displays the public properties and values of the properties (recursively) in a TreeView. Even a naive implementation is ok, I just need something to start with.
The solution must be in WPF, not winforms or com, etc...
So I took parts from Chris Taylor's example and the structure of a codeproject article and merged them into this:
TreeView xaml:
<TreeView Name="tvObjectGraph" ItemsSource="{Binding FirstGeneration}" Margin="12,41,12,12" FontSize="13" FontFamily="Consolas">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Name}" Grid.Column="0" Grid.Row="0" Padding="2,0" />
<TextBlock Text="{Binding Type}" Grid.Column="1" Grid.Row="0" Padding="2,0" />
<TextBlock Text="{Binding Value}" Grid.Column="2" Grid.Row="0" Padding="2,0" />
</Grid>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Wire-up code
void DisplayObjectGraph(object graph)
{
var hierarchy = new ObjectViewModelHierarchy(graph);
tvObjectGraph.DataContext = hierarchy;
}
ObjectViewModel.cs:
public class ObjectViewModel : INotifyPropertyChanged
{
ReadOnlyCollection<ObjectViewModel> _children;
readonly ObjectViewModel _parent;
readonly object _object;
readonly PropertyInfo _info;
readonly Type _type;
bool _isExpanded;
bool _isSelected;
public ObjectViewModel(object obj)
: this(obj, null, null)
{
}
ObjectViewModel(object obj, PropertyInfo info, ObjectViewModel parent)
{
_object = obj;
_info = info;
if (_object != null)
{
_type = obj.GetType();
if (!IsPrintableType(_type))
{
// load the _children object with an empty collection to allow the + expander to be shown
_children = new ReadOnlyCollection<ObjectViewModel>(new ObjectViewModel[] { new ObjectViewModel(null) });
}
}
_parent = parent;
}
public void LoadChildren()
{
if (_object != null)
{
// exclude value types and strings from listing child members
if (!IsPrintableType(_type))
{
// the public properties of this object are its children
var children = _type.GetProperties()
.Where(p => !p.GetIndexParameters().Any()) // exclude indexed parameters for now
.Select(p => new ObjectViewModel(p.GetValue(_object, null), p, this))
.ToList();
// if this is a collection type, add the contained items to the children
var collection = _object as IEnumerable;
if (collection != null)
{
foreach (var item in collection)
{
children.Add(new ObjectViewModel(item, null, this)); // todo: add something to view the index value
}
}
_children = new ReadOnlyCollection<ObjectViewModel>(children);
this.OnPropertyChanged("Children");
}
}
}
/// <summary>
/// Gets a value indicating if the object graph can display this type without enumerating its children
/// </summary>
static bool IsPrintableType(Type type)
{
return type != null && (
type.IsPrimitive ||
type.IsAssignableFrom(typeof(string)) ||
type.IsEnum);
}
public ObjectViewModel Parent
{
get { return _parent; }
}
public PropertyInfo Info
{
get { return _info; }
}
public ReadOnlyCollection<ObjectViewModel> Children
{
get { return _children; }
}
public string Type
{
get
{
var type = string.Empty;
if (_object != null)
{
type = string.Format("({0})", _type.Name);
}
else
{
if (_info != null)
{
type = string.Format("({0})", _info.PropertyType.Name);
}
}
return type;
}
}
public string Name
{
get
{
var name = string.Empty;
if (_info != null)
{
name = _info.Name;
}
return name;
}
}
public string Value
{
get
{
var value = string.Empty;
if (_object != null)
{
if (IsPrintableType(_type))
{
value = _object.ToString();
}
}
else
{
value = "<null>";
}
return value;
}
}
#region Presentation Members
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (_isExpanded != value)
{
_isExpanded = value;
if (_isExpanded)
{
LoadChildren();
}
this.OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
if (_isExpanded && _parent != null)
{
_parent.IsExpanded = true;
}
}
}
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
}
}
public bool NameContains(string text)
{
if (String.IsNullOrEmpty(text) || String.IsNullOrEmpty(Name))
{
return false;
}
return Name.IndexOf(text, StringComparison.InvariantCultureIgnoreCase) > -1;
}
public bool ValueContains(string text)
{
if (String.IsNullOrEmpty(text) || String.IsNullOrEmpty(Value))
{
return false;
}
return Value.IndexOf(text, StringComparison.InvariantCultureIgnoreCase) > -1;
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
ObjectViewModelHierarchy.cs:
public class ObjectViewModelHierarchy
{
readonly ReadOnlyCollection<ObjectViewModel> _firstGeneration;
readonly ObjectViewModel _rootObject;
public ObjectViewModelHierarchy(object rootObject)
{
_rootObject = new ObjectViewModel(rootObject);
_firstGeneration = new ReadOnlyCollection<ObjectViewModel>(new ObjectViewModel[] { _rootObject });
}
public ReadOnlyCollection<ObjectViewModel> FirstGeneration
{
get { return _firstGeneration; }
}
}
Well, this is probably a little more naive than you where hoping for, but it could possibly give you a starting point. It could do with some refactoring, but it was literally done in 15 min so take it for what it is, which is not well tested or using any WPF fancies for that matter.
First a simple UserControl which just hosts a TreeView
<UserControl x:Class="ObjectBrowser.PropertyTree"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<TreeView Name="treeView1" TreeViewItem.Expanded="treeView1_Expanded" />
</Grid>
</UserControl>
The code behind for this will have just one property called ObjectGraph, this is set to the instance of the object you want to browse.
The tree only gets loaded with the first level of properties each node has the format PropertyName : Value or PropertyName : Type, if the property is a primitive type (see the IsPrimitive function), then the value is shown, otherwise an empty string is added as the child node. Adding the empty string indicates to the user that the node can ge expanded.
When the node is exanded a quick check is done to see if the first child is an empty string, if it is then the node is cleared and the properties for that node loaded into the tree.
So this basically builds the tree up as the node are expanded. This makes like easier for two reasons
1- No need to perform recursion
2- No need to detect cyclic references which will expand to eternity or some resource is depleted, which ever comes first.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Reflection;
namespace ObjectBrowser
{
public partial class PropertyTree : UserControl
{
public PropertyTree()
{
InitializeComponent();
}
private void treeView1_Expanded(object sender, RoutedEventArgs e)
{
TreeViewItem item = e.OriginalSource as TreeViewItem;
if (item.Items.Count == 1 && item.Items[0].ToString() == string.Empty)
{
LoadGraph(item.Items, item.Tag);
}
}
public object ObjectGraph
{
get { return (object)GetValue(ObjectGraphProperty); }
set { SetValue(ObjectGraphProperty, value); }
}
public static readonly DependencyProperty ObjectGraphProperty =
DependencyProperty.Register("ObjectGraph", typeof(object), typeof(PropertyTree),
new UIPropertyMetadata(0, OnObjectGraphPropertyChanged));
private static void OnObjectGraphPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
PropertyTree control = source as PropertyTree;
if (control != null)
{
control.OnObjectGraphChanged(source, EventArgs.Empty);
}
}
protected virtual void OnObjectGraphChanged(object sender, EventArgs e)
{
LoadGraph(treeView1.Items, ObjectGraph);
}
private void LoadGraph(ItemCollection nodeItems, object instance)
{
nodeItems.Clear();
if (instance == null) return;
Type instanceType = instance.GetType();
foreach (PropertyInfo pi in instanceType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
object propertyValue =pi.GetValue(instance, null);
TreeViewItem item = new TreeViewItem();
item.Header = BuildItemText(instance, pi, propertyValue);
if (!IsPrimitive(pi) && propertyValue != null)
{
item.Items.Add(string.Empty);
item.Tag = propertyValue;
}
nodeItems.Add(item);
}
}
private string BuildItemText(object instance, PropertyInfo pi, object value)
{
string s = string.Empty;
if (value == null)
{
s = "<null>";
}
else if (IsPrimitive(pi))
{
s = value.ToString();
}
else
{
s = pi.PropertyType.Name;
}
return pi.Name + " : " + s;
}
private bool IsPrimitive(PropertyInfo pi)
{
return pi.PropertyType.IsPrimitive || typeof(string) == pi.PropertyType;
}
}
}
Using the control is quite simple. Here I will just put the control on Form and then set the ObjectGraph to an instance of an object, I arbitrarily chose XmlDataProvider.
XAML
<Window x:Class="ObjectBrowser.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" xmlns:my="clr-namespace:ObjectBrowser" Loaded="Window_Loaded">
<Grid>
<my:PropertyTree x:Name="propertyTree1" />
</Grid>
</Window>
The code behind
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace ObjectBrowser
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var o = new XmlDataProvider();
o.Source = new Uri("http://www.stackoverflow.com");
propertyTree1.ObjectGraph = o;
}
}
}
Of course this would still need a lot of work, special handling for types like arrays possibly a mechanism to handle custom views to special types etc.
Related
I have an TreeView that is bound to an class like this:
MainWindow.xaml
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ResourceDictionary>
<Style x:Key="TreeViewItemStyle" TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded}" />
<Setter Property="IsSelected" Value="{Binding IsInitiallySelected, Mode=OneTime}" />
<Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
</Style>
<HierarchicalDataTemplate x:Key="CheckBoxItemTemplate"
ItemsSource="{Binding Children.Values, Mode=OneTime}">
<StackPanel Orientation="Horizontal">
<CheckBox Focusable="False"
IsChecked="{Binding IsChecked}"
VerticalAlignment="Center" />
<TextBlock Text="{Binding Name, StringFormat='({0})'}"
Margin="5,2,2,5"/>
</StackPanel>
</HierarchicalDataTemplate>
</ResourceDictionary>
</Window.Resources>
<TreeView Grid.Column="0"
x:Name="MyTree"
ItemContainerStyle="{StaticResource TreeViewItemStyle}"
ItemsSource="{Binding Mode=OneTime}"
ItemTemplate="{StaticResource CheckBoxItemTemplate}"
Margin="5"
FontSize="14"
Padding ="10"
Width ="400"/>
</Window>
MainWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Data;
namespace WpfApplication2
{
/// <summary>
/// Interaktionslogik für MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
Foo Root;
public MainWindow()
{
ObjectDataProvider ODP = new ObjectDataProvider();
ODP.MethodName = "CreateFoos";
ODP.ObjectType = typeof(Foo);
DataContext = ODP;
InitializeComponent();
Root = MyTree.Items[0] as Foo;
Root.StatusChanged += Root_StatusChanged;
MyTree.Focus();
}
void Root_StatusChanged(object sender, EventArgs e)
{
Console.WriteLine("Changed!");
}
}
}
Foo.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace WpfApplication2
{
public class Foo : INotifyPropertyChanged
{
public event EventHandler StatusChanged;
bool? _isChecked = false;
public Dictionary<int, Foo> Children { get; set; }
string _Name = "";
Foo _parent;
Foo(string MyName)
{
_Name = MyName;
_isChecked = false;
Children = new Dictionary<int, Foo>();
}
void Initialize()
{
foreach (Foo child in this.Children.Values)
{
child._parent = this;
child.Initialize();
}
}
public static List<Foo> CreateFoos()
{
Foo Base = new Foo("Test");
Base.Children.Add(1, new Foo("Nr 1"));
Base.Children.Add(2, new Foo("Nr 2"));
Base.Children.Add(3, new Foo("Nr 3"));
Base.Children[1].Children.Add(1, new Foo("Nr 1-1"));
Base.Children[1].Children.Add(2, new Foo("Nr 1-2"));
Base.Children[1].Children.Add(3, new Foo("Nr 1-3"));
Base.Children[2].Children.Add(1, new Foo("Nr 2-1"));
Base.Children[2].Children.Add(2, new Foo("Nr 2-2"));
Base.Children[2].Children.Add(3, new Foo("Nr 2-3"));
Base.Children[3].Children.Add(1, new Foo("Nr 3-1"));
Base.Children[3].Children.Add(2, new Foo("Nr 3-2"));
Base.Children[3].Children.Add(3, new Foo("Nr 3-3"));
Base.Initialize();
return new List<Foo> { Base };
}
public string Name
{
get
{
return _Name;
}
set
{
_Name = value;
OnPropertyChanged("Name");
}
}
public bool? IsChecked
{
get { return _isChecked; }
set { SetIsChecked(value, true, true); }
}
void SetIsChecked(bool? value, bool updateChildren, bool updateParent)
{
if (value == _isChecked)
return;
_isChecked = value;
if (updateChildren && _isChecked.HasValue)
{
foreach (int Key in Children.Keys)
{
Children[Key].SetIsChecked(_isChecked, true, false);
}
}
if (updateParent && _parent != null)
{
_parent.VerifyCheckState();
}
OnPropertyChanged("IsChecked");
EventHandler MyHandler = StatusChanged;
if (null != MyHandler)
{
MyHandler.Invoke(this, EventArgs.Empty);
}
}
void VerifyCheckState()
{
bool? state = null;
bool CheckedFound = false;
bool UncheckedFound = false;
bool NullFound = false;
foreach (int Key in Children.Keys)
{
if (Children[Key].IsChecked == true)
{
CheckedFound = true;
}
else if (Children[Key].IsChecked == false)
{
UncheckedFound = true;
}
else
{
NullFound = true;
}
}
if (CheckedFound && !UncheckedFound && !NullFound)
{
state = true;
}
else if (!CheckedFound && UncheckedFound && !NullFound)
{
state = false;
}
SetIsChecked(state, false, true);
}
void OnPropertyChanged(string prop)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
This is the result:
My Prolem now is, that the Event only fires when the status of the root node changes. But when I check/uncheck only Sub-Items the root-node not always changes, so I don't get this changes.
How do I have to change my code to get all changes, but also don't fire multiple events, because when I change one item multile other items (parent and children) can change also.
I am displaying data with a structure similar to the folder structure on a disc and I want to filter it. If an item is unselected all items under that one are unselected too. If an item low in the tree is selected, all parents to up to the root are selected. The selection shall result in reloading and displaying the data matching the selected items.
For this I have created a TreeFilter class which is displayed a standard WPF TreeView.
When the selection is changed I want to build a list with selected items in the tree (I will extend the TreeFilter class with an additional property for that)
As the combination of items is fixed I created a static class which returns predefined sets of filters (trees).
I even managed to put the whole thing into a UserControl (I am still relatively new to c# and WPF) to be able to reuse it as I will need it in different parts of the solution. The control has a dependency property to bind the filter tree.
I don’t want to use code behind, as everything is MVVM
My problem is the following: How do I even notice, when an item is selected so I can reflect that change in updating the data? When any of those checkboxes is changed (on/off) I want to update the data-view
Thank you very much for your patience and support!
This is my code:
The TreeFilter class
public class DTTreeFilter : INotifyPropertyChanged
{
public string Caption { get; set; }
public string Description { get; set; }
bool isEnabled;
public bool IsEnabled
{
get => isEnabled;
set
{
if (value != this.isEnabled)
{
this.isEnabled = value;
if (this.isEnabled == false)
{
foreach (var child in children)
{
child.IsEnabled = false;
}
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsEnabled)));
}
}
}
private bool isExpanded;
public bool IsExpanded
{
get { return isExpanded; }
set
{
if (value != isExpanded)
{
isExpanded = value;
NotifyPropertyChanged(nameof(IsExpanded));
}
}
}
private bool isSelected;
public bool IsSelected
{
get { return isSelected; }
set
{
if (value != isSelected)
{
isSelected = value;
NotifyPropertyChanged(nameof(IsSelected));
}
}
}
private bool isFilterSelected;
public bool IsFilterSelected
{
get { return isFilterSelected; }
set
{
if (value != isFilterSelected)
{
isFilterSelected = value;
if (this.isFilterSelected == false)
{
foreach (var child in children)
{
child.IsFilterSelected = false;
}
}
// Filter ein -> alle Eltern einschalten
if (this.isFilterSelected == true)
{
if (this.Parent != null)
this.Parent.IsFilterSelected = true;
}
NotifyPropertyChanged(nameof(IsFilterSelected));
}
}
}
public DTTreeFilter Parent { get; set; }
ObservableCollection<DTTreeFilter> children;
public ObservableCollection<DTTreeFilter> Children
{
get => children;
set => children = value;
}
public static DTTreeFilter Create(string Caption, string Description, DTTreeFilter Parent = null)
{
return new DTTreeFilter(Caption, Description, Parent);
}
public DTTreeFilter(string Caption, string Description, DTTreeFilter Parent = null)
{
children = new ObservableCollection<DTTreeFilter>();
this.Caption = Caption;
this.Description = Description;
this.IsEnabled = true;
if (Parent != null)
this.Parent = Parent;
}
public void AddChild(DTTreeFilter child)
{
child.Parent = this;
children.Add(child);
this.IsExpanded = true;
}
public override string ToString()
{
return $"ToString {this.Caption }";
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
The TreeView
<TreeView
x:Name="trFilter"
ItemsSource="{Binding FilterList}" >
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:DTTreeFilter}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsFilterSelected}"
IsEnabled="{Binding IsEnabled}"
VerticalContentAlignment="Center"/>
<TextBlock Text="{Binding Caption}"
VerticalAlignment="Center"
FontWeight="DemiBold"
FontSize="14"
/>
<TextBlock Text=" " />
<TextBlock Text="{Binding Description}"
VerticalAlignment="Center"
Width="250"
FontSize="11"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
<Setter Property="IsExpanded" Value="{Binding IsExpanded}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
The prepared Filterset
public static DTTreeFilter GetFilterSetA()
{
DTTreeFilter rootItem = DTTreeFilter.Create("Root Item", "this is the root item");
DTTreeFilter childItemA = DTTreeFilter.Create("Child A", "this is child A");
DTTreeFilter childItemB = DTTreeFilter.Create("Child B", "this is child B");
rootItem.AddChild(childItemA);
rootItem.AddChild(childItemB);
DTTreeFilter childItemA1 = DTTreeFilter.Create("Child A1", "this is child A1");
DTTreeFilter childItemA2 = DTTreeFilter.Create("Child A1", "this is child A2");
childItemA.AddChild(childItemA1);
childItemA.AddChild(childItemA2);
return rootItem;
}
The ViewModel
public class TreeFilterTestViewModel
{
public virtual ObservableCollection<DTTreeFilter> AllFilters { get; set; } = new ObservableCollection<DTTreeFilter>();
public TreeFilterTestViewModel()
{
AllFilters.Add( DTSystemFilter.GetFilterSetA() );
}
resulting in
enter image description here
I am trying to bind a collection of custom objects to a tree view's ItemSource in WPF with no success.
Here is the MainWindow.xaml:
<Window
x:Class="AoiImageLift.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:AoiImageLift.UI.ViewModels"
Height="500"
Width="500">
<Window.DataContext>
<vm:MainWindowViewModel/>
</Window.DataContext>
<TreeView ItemsSource="{Binding TreeViewViewModel.ProcessList}"/>
</Window>
Here is the App.xaml:
</Application>
</Application.Resources>
<!-- TreeView Style -->
<Style TargetType="{x:Type TreeView}">
<Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Visible"/>
<Setter Property="SelectedValuePath" Value="Wafer"/>
<Setter Property="ItemTemplate">
<Setter.Value>
<HierarchicalDataTemplate ItemsSource="{Binding ProcessList}">
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock
FontFamily="SegoeUI"
Foreground="MidnightBlue"
Text="{Binding Wafer}"/>
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
<TextBlock
Text="{Binding ProcessNumber}"
FontFamily="SegoeUI"
Foreground="MidnightBlue"/>
</HierarchicalDataTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources>
</Application>
Here is the MainWindowViewModel.cs:
public class MainWindowViewModel : ViewModel
{
private WaferSelectionTreeViewViewModel treeViewViewModel;
public MainWindowViewModel()
{
BackgroundWorker initThread = new BackgroundWorker();
initThread.DoWork += (sender, e) =>
{
e.Result = new SingulationOneTable().GetWaferList();
};
initThread.RunWorkerCompleted += (sender, e) =>
{
TreeViewViewModel = new WaferSelectionTreeViewViewModel(
(List<string>) e.Result);
};
initThread.RunWorkerAsync();
}
public WaferSelectionTreeViewViewModel TreeViewViewModel
{
get { return treeViewViewModel; }
set
{
treeViewViewModel = value;
OnPropertyChanged("TreeViewViewModel");
}
}
}
FYI, this line of code...
e.Result = new SingulationOneTable().GetWaferList();
...simply returns a large list of strings. That list of strings is then passed into the constructor of the WaferSelectionTreeViewViewModel class.
Here is the WaferSelectionTreeViewViewModel.cs:
public class WaferSelectionTreeViewViewModel : ViewModel
{
private ObservableCollection<Process> processList;
public class TreeViewItemBase : ViewModel
{
private bool isSelected;
public bool IsSelected
{
get { return isSelected; }
set
{
if (value != isSelected)
{
isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
private bool isExpanded;
public bool IsExpanded
{
get { return isExpanded; }
set
{
if (value != isExpanded)
{
isExpanded = value;
OnPropertyChanged("IsExpanded");
}
}
}
}
public class Process : TreeViewItemBase
{
private string name;
public Process(string name)
{
this.name = name;
this.Children = new ObservableCollection<string>();
}
public string Name { get { return name; } }
public ObservableCollection<string> Children { get; set; }
}
public WaferSelectionTreeViewViewModel(List<string> waferList)
{
processList = new ObservableCollection<Process>();
List<string> procList = new List<string>();
foreach (string wafer in waferList)
{
procList.Add(wafer.Substring(0, 4));
}
IEnumerable<string> distintProcessList = procList.Distinct();
foreach (string process in distintProcessList)
{
Process newProcess = new Process(process);
List<string> wafersInProcess = waferList.FindAll(
x => x.Substring(0, 4) == process);
foreach (string waferInThisProcess in wafersInProcess)
{
newProcess.Children.Add(waferInThisProcess);
}
}
}
public ObservableCollection<Process> ProcessList
{
get
{
return processList;
}
set
{
processList = value;
OnPropertyChanged("ProcessList");
}
}
}
Can anyone figure out why the items are not showing up in the tree view??
Regards,
Kyle
There are a couple of errors in your bindings. If you run your application from Visual Studio, you'll probably see one or more messages like this in the Output window:
System.Windows.Data Error: 40 : BindingExpression path error: (...)
Firstly, each root item is bound to a Process object from TreeViewViewModel.ProcessList and it's told to display a property called ProcessNumber, but there is no such property, at least not in the code you posted. So I'm guessing the process items are showing up in the TreeView, but they're blank.
Secondly, your HierarchicalItemTemplate says that the list of child items can be found in a property called ProcessList. But each root item is a Process, which doesn't have that property, so no child items are displayed. You probably mean:
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
Now, Process.Children is a simple list of strings, so you don't need to include the <HierarchicalDataTemplate.ItemTemplate> part (and of course, strings don't have the Wafer property that template is looking for).
I have written a multi select behavior based on this post:WPF TreeView with Multiple Selection
However my behavior does't seem to work as expected..Can you please help?
The following is my xaml code:
<Window x:Class="TreeView.Spike.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Spike="clr-namespace:TreeView.Spike"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*"/>
<ColumnDefinition Width=".5*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="40"></RowDefinition>
</Grid.RowDefinitions>
<TreeView ItemsSource="{Binding Nodes}" Grid.Row="0" x:Name="treeView" Grid.Column="0">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Nodes}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}">
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add"></MenuItem>
<MenuItem Header="Delete"></MenuItem>
</ContextMenu>
</TreeView.ContextMenu>
<i:Interaction.Behaviors>
<Spike:MultipleItemSelectionAttachedBehavior AllSelectedItems="{Binding Path=AllSelectedNodes}"/>
</i:Interaction.Behaviors>
</TreeView>
</Grid>
</Window>
My attached behavior:
public class MultipleItemSelectionAttachedBehavior:Behavior<System.Windows.Controls.TreeView>
{
public static DependencyProperty AllSelectedItemsProperty =
DependencyProperty.RegisterAttached("AllSelectedItems", typeof(object), typeof(MultipleItemSelectionAttachedBehavior),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Inherits));
private static readonly PropertyInfo IsSelectionChangeActiveProperty = typeof(System.Windows.Controls.TreeView).GetProperty("IsSelectionChangeActive",
BindingFlags.NonPublic | BindingFlags.Instance);
public object AllSelectedItems
{
get
{
return (object)GetValue(AllSelectedItemsProperty);
}
set
{
SetValue(AllSelectedItemsProperty, value);
}
}
public static bool GetAllSelectedItems(DependencyObject obj)
{
return (bool)obj.GetValue(AllSelectedItemsProperty);
}
public static void SetAllSelectedItems(DependencyObject obj, bool value)
{
obj.SetValue(AllSelectedItemsProperty, value);
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += AssociatedObject_SelectedItemChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectedItemChanged -= AssociatedObject_SelectedItemChanged;
}
void AssociatedObject_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (IsSelectionChangeActiveProperty == null) return;
var selectedItems = new List<Node>();
var treeViewItem = AssociatedObject.SelectedItem as Node;
if (treeViewItem == null) return;
// allow multiple selection
// when control key is pressed
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
var isSelectionChangeActive = IsSelectionChangeActiveProperty.GetValue(AssociatedObject, null);
IsSelectionChangeActiveProperty.SetValue(AssociatedObject, true, null);
selectedItems.ForEach(item => item.IsSelected = true);
IsSelectionChangeActiveProperty.SetValue(AssociatedObject, isSelectionChangeActive, null);
}
else
{
// deselect all selected items except the current one
selectedItems.ForEach(item => item.IsSelected = (item == treeViewItem));
selectedItems.Clear();
}
if (!selectedItems.Contains(treeViewItem))
{
selectedItems.Add(treeViewItem);
}
else
{
// deselect if already selected
treeViewItem.IsSelected = false;
selectedItems.Remove(treeViewItem);
}
AllSelectedItems = selectedItems;
}
}
..and my ViewModel
public class ViewModel :NotificationObject
{
public ViewModel()
{
AllSelectedNodes = new ObservableCollection<Node>();
}
private ObservableCollection<Node> _allSelectedNodes;
public ObservableCollection<Node> AllSelectedNodes
{
get { return _allSelectedNodes; }
set
{
_allSelectedNodes = value;
RaisePropertyChanged(() => AllSelectedNodes);
}
}
}
My Model:
public class Node:NotificationObject
{
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChanged(() => Name);
}
}
private bool _isExpanded = true;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
_isExpanded = value;
RaisePropertyChanged(() => IsExpanded);
}
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
RaisePropertyChanged(() => IsSelected);
}
}
private ObservableCollection<Node> _nodes;
public ObservableCollection<Node> Nodes
{
get { return _nodes; }
set
{
_nodes = value;
RaisePropertyChanged(() => Nodes);
}
}
public static IList<Node> Create()
{
return new List<Node>()
{
new Node()
{
Name = "Activity",
Nodes = new ObservableCollection<Node>()
{
new Node() {Name = "Company",Nodes = new ObservableCollection<Node>(){ new Node() {Name = "Company1",Existing = false}}},
new Node() {Name = "Strategy",Nodes = new ObservableCollection<Node>(){ new Node() {Name = "Strategy1"}}},
new Node() {Name = "Vehicle",Nodes = new ObservableCollection<Node>(){ new Node() {Name = "Vehicle1",Existing = false}}}
}
}
};
}
}
..and my initialization clode:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var viewModel = new ViewModel();
this.DataContext = viewModel;
viewModel.Nodes = new ObservableCollection<Node>(Node.Create());
}
}
The issue is that, it still selects only one while it's supposed to select multiple while holding back control key.
In AssociatedObject_SelectedItemChanged, how is the selectedItems count ever anything but zero when you just instantiate it or 1 when you add the treeViewItem to it a bit later? Try setting it to the AllSelectedItems property. Also, shouldn't the AllSelectedItems property be a list or IEnumerable of some sort?
if(AllSelectedItems == null)
{
AllSelectedItems = new List<Node>();
}
List<Node> selectedItems = AllSelectedItems;
you are missing the binding of the IsSelected property:
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</TreeView.ItemContainerStyle>
you need to add Selection mode property to treeview like this
SelectionMode="Multiple"
I tried the solution discussed here: WPF treeview itemselected moves incorrectly when deleting an item
However, I'm still seeing the selection jump to that parent when deleting an item. What am I doing wrong?
MainWindow.xaml
<Window x:Class="TreeViewDelete.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Tree View Delete sample" Height="350" Width="525">
<Window.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding Selected, Mode=TwoWay}"/>
<EventSetter Event="KeyDown" Handler="OnTreeKeyDown"/>
</Style>
<HierarchicalDataTemplate x:Key="recursiveTemplate" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<TreeView Name="m_tree" ItemsSource="{Binding Children}" ItemTemplate="{StaticResource recursiveTemplate}"/>
</Grid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private Container m_root, m_child;
public MainWindow()
{
InitializeComponent();
m_root = new Container("Root");
m_child = new Container("main");
m_child.Add(new Container("k1"));
m_child.Add(new Container("k2"));
m_child.Add(new Container("k3"));
m_child.Add(new Container("k4"));
m_child.Add(new Container("k5"));
m_root.Add(m_child);
m_tree.DataContext = m_root;
}
private IEnumerable<T> GetVisualAncestorsOfType<T>(DependencyObject obj) where T : DependencyObject
{
for (; obj != null; obj = VisualTreeHelper.GetParent(obj))
if (obj is T)
yield return (T)obj;
}
private void OnTreeKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Delete:
{
TreeViewItem item = sender as TreeViewItem;
if (item != null)
{
Container container = item.Header as Container;
if (container != null)
{
Container parent = container.Parent;
// Find the currently focused element in the TreeView's focus scope
DependencyObject focused =
FocusManager.GetFocusedElement(
FocusManager.GetFocusScope(m_tree)) as DependencyObject;
// Scan up the VisualTree to find the TreeViewItem for the parent
var parentContainer = (
from element in GetVisualAncestorsOfType<FrameworkElement>(focused)
where (element is TreeViewItem && element.DataContext == parent)
|| element is TreeView
select element
).FirstOrDefault();
parent.Remove(container);
if (parentContainer != null)
{
parent.Children[0].Selected = true;
parentContainer.Focus();
}
}
}
e.Handled = true;
}
break;
}
}
}
and finally Container.cs
public class Container : INotifyPropertyChanged
{
private bool m_selected;
private string m_name;
private ObservableCollection<Container> m_children;
private Container m_parent;
public Container(string name)
{
m_name = name;
m_children = new ObservableCollection<Container>();
m_parent = null;
m_selected = false;
}
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
get
{
return m_name;
}
set
{
m_name = value;
OnPropertyChanged("Name");
}
}
public Container Parent
{
get
{
return m_parent;
}
set
{
m_parent = value;
}
}
public bool Selected
{
get
{
return m_selected;
}
set
{
m_selected = value;
OnPropertyChanged("Selected");
}
}
public ObservableCollection<Container> Children
{
get
{
return m_children;
}
}
private void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public void Add(Container child)
{
m_children.Add(child);
child.Parent = this;
}
public void Remove(Container child)
{
m_children.Remove(child);
child.Parent = null;
}
}
I didn't check this myself but i think you can make this happen by using the CollectionChanged event of the ObservableCollections. here you can attach an event to your children collection so that when one of them is deleted you go and set the Selected property of for example the first child of the collection to true. something like the following code:
attach the event:
Children.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Children_CollectionChanged);
event implementation:
void Children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
Children.First().Selected = true;
}
}
I tried the solution supplied here: "TreeView selected item when removing nodes" and in a simple application it works beautifully. Needless to say in the app I'm working on it doesn't...
Looking at what the article is talking about, it seems I need the TreeViewItem that is the container of my item. So I tried:
m_tree.ItemContainerGenerator.ContainerFromItem
Except this always returns null. Then I read the following: TreeView.ItemContainerGenerator.ContainerFromItem returns null, at which point my brain nearly blew a fuse!
It seems to get at the TreeViewItem I want to be selected, I have to start at the top of my hierarchy and work down the tree until I get to where I want. So, my container data has a Parent property, so I built a stack of objects:
Stack<Containerl> stack = new Stack<Container>();
Container toBeSelected = ... my object to be selected after deletion ...
while (toBeSelected != null)
{
stack.Push(toBeSelected);
toBeSelected = toBeSelected.Parent;
}
then I delete all the items from my hierarchy, then do the following:
TreeViewItem item = m_tree.ItemContainerGenerator.ContainerFromItem(stack.Pop()) as TreeViewItem;
while(item != null && (stack.Count > 0))
{
item = item.ItemContainerGenerator.ContainerFromItem(stack.Pop()) as TreeViewItem;
}
// Force this item to be selected, and set focus
item.IsSelected = true;
item.Focus();
It works!!!