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.
Related
I imagine that this is going to be flagged as a dduplicate but I am kind of stuck and not sure how to interpret other answers, or really know what I am doing wrong. Essentially my end problem is this:
I have a class that contains a field (a number) that can be changed. This class will be in an observable list that should contain a total of all the numbers from all elements in the observable collection. The total number should automatically change when the field of any member of the list is changed by the user. It is actually very similar to the problem posed here: Updating calculated total when a property on the underlying item in a list is changed - wpf. However, when I followed that solution and created a textbox that was bound to the total, the total was never changed. It seems also that the OnPropertyChanged event for the collection is never called to indicate that a property was changed in the underlying list. I think the problem is related to the fact that total_dose is never actually set but just has a get (from the link I provided). I am not sure how to implement the solutions I see on other posts. Would appreciate some guidance
CS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Collections.Specialized;
// https://stackoverflow.com/questions/51331010/updating-calculated-total-when-a-property-on-the-underlying-item-in-a-list-is-ch
namespace WpfApp2
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ObservableCollection<string> sub_list { get; set; }
public MainWindow()
{
InitializeComponent();
structure temp_struct = new structure("Bladder");
structure_info_list.ItemsSource = temp_struct.fx_list;
this.DataContext = temp_struct;
}
}
public class structure : INotifyPropertyChanged
{
public structure(string name)
{
this.name = name;
fx_list = new ObservableCollection<fraction>();
fraction fx1 = new fraction(3);
fraction fx2 = new fraction(4);
fraction fx3 = new fraction(5);
fx_list.Add(fx1);
fx_list.Add(fx2);
fx_list.Add(fx3);
MessageBox.Show("Total: " + total_dose);
fx_list[0].fx_dose = 50;
MessageBox.Show("Total: " + total_dose);
}
private void fractions_changed_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems)
{
((fraction)item).PropertyChanged += fx_Changed;
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var item in e.OldItems)
{
((fraction)item).PropertyChanged -= fx_Changed;
}
}
}
void fx_Changed(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(fraction.fx_dose))
{
OnPropertyChanged(nameof(fx_list));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
MessageBox.Show("A property in the underlying list was changed: " + propertyName);
}
/*
public void calc_total_sum()
{
int total_sum_temp = 0;
foreach (fraction fx in fx_list)
{
total_sum_temp += fx.fx_dose;
}
total_sum = total_sum_temp;
}
*/
public string name { get; set; }
public ObservableCollection<fraction> fx_list { get; set; }
//public int total_sum { get; set; }
public int total_dose
{
get { return fx_list.Sum(x => x.fx_dose); }
set { }
}
}
public class fraction : INotifyPropertyChanged
{
private int _fx_dose;
public int fx_dose
{
get { return _fx_dose; }
set
{
_fx_dose = value;
this.calc_eq();
this.OnPropertyChanged("fx_dose");
//MessageBox.Show("FX DOSE PROP");
}
}
private int _eq;
public int eq
{
get { return _eq; }
set { _eq = value; }
}
public event PropertyChangedEventHandler PropertyChanged;
public fraction(int fx_dose)
{
this.fx_dose = fx_dose;
this.eq = fx_dose*2;
}
public void calc_eq()
{
this.eq = this.fx_dose * 2;
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
//PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
XAML
<Window x:Class="WpfApp2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp2"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<ItemsControl Name="structure_info_list">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Fraction "/>
<TextBox Text="{Binding Path=fx_dose, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Total: "/>
<TextBlock Text="{Binding Path=total_dose, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</StackPanel>
</Window>
Edit: I found the solution by looking at section 3 of this link
https://www.codeproject.com/Articles/28098/A-WPF-Pie-Chart-with-Data-Binding-Support#three
There are a couple if items.
First, you are setting the datacontext and the itemsource after you populate the list - this causes the notifications to not reach your binding. it is better to set these up in your XAML file.
Next, your property total_does does not call OnPropertyChanged("total_does").
Also, the datacontext is connected to the observable collection within structure. total_does is outside of the observable collection so your window will not see total_does or its notifications.
I made adjustmnts to show what I mean:
CS file:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public ObservableCollection<string> sub_list { get; set; }
private structure temp_struct;
public MainWindow()
{
InitializeComponent();
temp_struct = new structure("Bladder");
}
public structure Temp_Struct
{
get => temp_struct;
set
{
temp_struct = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string memberName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(memberName));
}
}
public class structure : INotifyPropertyChanged
{
private ObservableCollection<fraction> fxlist = new ObservableCollection<fraction>();
public structure(string name)
{
this.name = name;
fx_list = new ObservableCollection<fraction>();
fraction fx1 = new fraction(3);
fraction fx2 = new fraction(4);
fraction fx3 = new fraction(5);
fx_list.Add(fx1);
fx_list.Add(fx2);
fx_list.Add(fx3);
MessageBox.Show("Total: " + total_dose);
OnPropertyChanged("total_dose");
fx_list[0].fx_dose = 50;
MessageBox.Show("Total: " + total_dose);
OnPropertyChanged("total_dose");
}
private void fractions_changed_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems)
{
((fraction)item).PropertyChanged += fx_Changed;
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var item in e.OldItems)
{
((fraction)item).PropertyChanged -= fx_Changed;
}
}
}
void fx_Changed(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(fraction.fx_dose))
{
OnPropertyChanged(nameof(fx_list));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new propertyChangedEventArgs(propertyName));
MessageBox.Show("A property in the underlying list was changed: " + propertyName);
}
/*
public void calc_total_sum()
{
int total_sum_temp = 0;
foreach (fraction fx in fx_list)
{
total_sum_temp += fx.fx_dose;
}
total_sum = total_sum_temp;
}
*/
public string name { get; set; }
public ObservableCollection<fraction> fx_list
{
get => fxlist;
set
{
fxlist = value;
OnPropertyChanged();
}
}
//public int total_sum { get; set; }
public int total_dose
{
get { return fx_list.Sum(x => x.fx_dose); }
}
}
public class fraction : INotifyPropertyChanged
{
private int _fx_dose;
public int fx_dose
{
get { return _fx_dose; }
set
{
_fx_dose = value;
this.calc_eq();
this.OnPropertyChanged("fx_dose");
//MessageBox.Show("FX DOSE PROP");
}
}
private int _eq;
public int eq
{
get { return _eq; }
set { _eq = value; }
}
public event PropertyChangedEventHandler PropertyChanged;
public fraction(int fx_dose)
{
this.fx_dose = fx_dose;
this.eq = fx_dose * 2;
}
public void calc_eq()
{
this.eq = this.fx_dose * 2;
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
//PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML file:
<Window
x:Name="WinMain"
x:Class="DeluxMeasureStudies.Windows.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DeluxMeasureStudies.Windows"
Title="MainWindow"
Width="800"
Height="450"
Foreground="White"
DataContext="{Binding Temp_Struct, ElementName=WinMain}"
Background="{StaticResource Normal.Window.Background}"
>
<StackPanel
Orientation="Vertical"
>
<ItemsControl Name="structure_info_list"
DataContext="{Binding Path=Temp_Struct, ElementName=WinMain}"
ItemsSource="{Binding Path=fx_list}">
<ItemsControl.ItemTemplate>
<DataTemplate
DataType="local:fraction"
>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Fraction "/>
<TextBox Text="{Binding Path=fx_dose, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Total: "/>
<TextBlock Text="{Binding Path=total_dose, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</StackPanel>
I spent some time on the internet for Treeview on wpf and found the following class.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Herdbook.Classes
{
public class TreeViewModel : INotifyPropertyChanged
{
TreeViewModel(string name)
{
Name = name;
Children = new List<TreeViewModel>();
}
#region Properties
public string Name { get; private set; }
public List<TreeViewModel> Children { get; private set; }
public bool IsInitiallySelected { get; private set; }
bool? _isChecked = false;
TreeViewModel _parent;
#region IsChecked
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) Children.ForEach(c => c.SetIsChecked(_isChecked, true, false));
if (updateParent && _parent != null) _parent.VerifyCheckedState();
NotifyPropertyChanged("IsChecked");
}
void VerifyCheckedState()
{
bool? state = null;
for (int i = 0; i < Children.Count; ++i)
{
bool? current = Children[i].IsChecked;
if (i == 0)
{
state = current;
}
else if (state != current)
{
state = null;
break;
}
}
SetIsChecked(state, false, true);
}
#endregion
#endregion
void Initialize()
{
foreach (TreeViewModel child in Children)
{
child._parent = this;
child.Initialize();
}
}
public static List<TreeViewModel> SetTree(string topLevelName)
{
List<TreeViewModel> treeView = new List<TreeViewModel>();
TreeViewModel tv = new TreeViewModel(topLevelName);
treeView.Add(tv);
#region Test Data
//Doing this below for this example, you should do it dynamically
//***************************************************
TreeViewModel tvChild1 = new TreeViewModel("منوی 1");
TreeViewModel tvChild2 = new TreeViewModel("منوی 2");
TreeViewModel tvChild3 = new TreeViewModel("منوی 3");
tv.Children.Add(tvChild1);
tv.Children.Add(tvChild2);
tv.Children.Add(tvChild3);
tvChild2.Children.Add(new TreeViewModel("زیرمنو"));
tvChild2.Children.Add(new TreeViewModel("زیر منو2"));
tvChild3.Children.Add(new TreeViewModel("زیر منوی"));
//***************************************************
#endregion
tv.Initialize();
return treeView;
}
public static List<string> GetTree()
{
List<string> selected = new List<string>();
return selected;
//***********************************************************
//From your window capture selected your treeview control like: TreeViewModel root = (TreeViewModel)TreeViewControl.Items[0];
// List<string> selected = new List<string>(TreeViewModel.GetTree());
//***********************************************************
}
#region INotifyPropertyChanged Members
void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
}
I have the following tables and viwe in the database (SQL Server 2014)
CREATE TABLE [dbo].[Menu](
[MenuID] [smallint] NOT NULL,
[NenuName] [nvarchar](50) NOT NULL,
[MenuLevel] [smallint] NULL,
CONSTRAINT [PK_Menu] PRIMARY KEY CLUSTERED
CREATE TABLE [dbo].[UserAccess](
[UserID] [tinyint] NOT NULL,
[MenuID] [smallint] NOT NULL,
CONSTRAINT [PK_UserAccess] PRIMARY KEY CLUSTERED
CREATE VIEW [dbo].[VwMenuList]
AS
SELECT MenuID, NenuName, MenuLevel,
(select COUNT(MenuID) from Menu as m2 where m1.MenuID=m2.MenuLevel) as childcount
FROM Menu as m1
1. How can I retrieve menu table information from the database for display in Treeview?
2. How do I display the information recorded in the useraccess table in Treeview? (Check Treeview nodes based on the useraccess table)
3. How to save Treeview changes to the useraccess table
Using the code below, I display a tree view in my window
<Window.Resources>
<ResourceDictionary>
<Style x:Key="TreeViewItemStyle" TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="False" />
<Setter Property="IsSelected" Value="{Binding IsInitiallySelected, Mode=OneTime}" />
<Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
</Style>
<HierarchicalDataTemplate x:Key="CheckBoxItemTemplate" ItemsSource="{Binding Children, Mode=OneTime}">
<StackPanel Orientation="Horizontal">
<CheckBox Focusable="False" IsChecked="{Binding IsChecked}" VerticalAlignment="Center" />
<ContentPresenter Content="{Binding Name, Mode=OneTime}" Margin="2,0" />
</StackPanel>
</HierarchicalDataTemplate>
</ResourceDictionary>
</Window.Resources>
<TreeView Height="157" HorizontalAlignment="Left" Margin="15,94,0,0" x:Name="treeView1"
VerticalAlignment="Top" Width="270"
ItemContainerStyle="{StaticResource TreeViewItemStyle}"
ItemTemplate="{StaticResource CheckBoxItemTemplate}"
BorderThickness="0" Background="#00000000" FlowDirection="RightToLeft" FontFamily="B koodak"/>
I have a ViewModel class that encapsulates an ObservableCollection of complex objects. I want to display these in a WPF DataGrid, one object per row, mapping object fields to columns.
For data validation, I want to check changes to one row against the properties of other rows, for example to check for unique values.
I have written a ValidationRule and added it to a BindingGroup on the DataGrid, but it is not called when items are edited. Why not? The rule is called when the grid is initialised.
XAML:
<Window x:Class="DataGridValidationTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DataGridValidationTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<DataGrid ItemsSource="{Binding Path=CBItems}"
AutoGenerateColumns="False"
CanUserDeleteRows="False">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="ValidationErrorTemplate">
<Setter.Value>
<ControlTemplate>
<Image
Source="/DataGridValidationTest;component/error.png"
ToolTip="{Binding RelativeSource={
RelativeSource FindAncestor,
AncestorType={x:Type DataGridRow}},
Path=(Validation.Errors)[0].ErrorContent}"
Margin="0"
Width="11" Height="11" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.Resources>
<DataGrid.BindingGroup>
<BindingGroup ValidatesOnNotifyDataError="True" NotifyOnValidationError="True">
<BindingGroup.ValidationRules>
<local:DuplicateValidationRule ValidatesOnTargetUpdated="True"/>
</BindingGroup.ValidationRules>
</BindingGroup>
</DataGrid.BindingGroup>
<DataGrid.Columns>
<DataGridTextColumn Header ="Id" Binding="{Binding Id}"/>
<DataGridTextColumn Header ="Value" Binding="{Binding Path=Value, UpdateSourceTrigger=PropertyChanged}"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Window>
Code behind:
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace DataGridValidationTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private ViewModel vm;
public MainWindow()
{
vm = new ViewModel();
DataContext = vm;
InitializeComponent();
vm.CBItems.Add(new ItemType { Id = "a", Value = 0 });
vm.CBItems.Add(new ItemType { Id = "b", Value = 1 });
vm.CBItems.Add(new ItemType { Id = "c", Value = 2 });
vm.CBItems.Add(new ItemType { Id = "d", Value = 3 });
vm.CBItems.Add(new ItemType { Id = "e", Value = 4 });
vm.CBItems.Add(new ItemType { Id = "f", Value = 5 });
vm.CBItems.Add(new ItemType { Id = "g", Value = 6 });
vm.CBItems.Add(new ItemType { Id = "h", Value = 7 });
vm.CBItems.Add(new ItemType { Id = "i", Value = 8 });
vm.CBItems.Add(new ItemType { Id = "j", Value = 9 });
}
}
public class DuplicateValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var bg = value as BindingGroup;
if (bg == null)
return new ValidationResult(true, null);
var dg = bg.Owner as DataGrid;
if (dg == null)
return new ValidationResult(true, null);
HashSet<string> usedValues = new HashSet<string>();
foreach (var item in dg.Items)
{
var itemValue = item as ItemType;
string ev = itemValue?.Value.ToString();
if (ev != null)
{
if (usedValues.Contains(ev))
{
return new ValidationResult(false, "Values must be unique.");
}
usedValues.Add(ev);
}
}
return new ValidationResult(true, null);
}
}
}
View model:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace DataGridValidationTest
{
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
}
public event PropertyChangedEventHandler PropertyChanged;
public virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private ObservableCollection<ItemType> cBItems = new ObservableCollection<ItemType>();
public ObservableCollection<ItemType> CBItems
{
get => cBItems;
set => cBItems = value;
}
}
public class ItemType : INotifyPropertyChanged
{
private string id = "";
public string Id
{
get => id;
set
{
id = value;
OnPropertyChanged("Id");
}
}
private int _value = 0;
public int Value
{
get => _value;
set
{
_value = value;
OnPropertyChanged("Value");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
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'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.