I need Wpf tree view like this - wpf

ViewModel has 2 field. Name, Childs
I need like this
1. When click on the root element, do 2 operation
first. expand yourself
second. select first child. If child element has childs, repeat 1.
otherwise do nothing
Only last child (leaf) selectable

UPDATE
Figured out a much better way to do this. This will also account for changes in the ObservableCollection.
The Xaml can just look like this
<Window.Resources>
<HierarchicalDataTemplate x:Key="Level1"
ItemsSource="{Binding Path=Childs}">
<TextBlock Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
</Window.Resources>
<TreeView ItemsSource="{Binding}"
...>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
And then we handle the IsSelected Property in the Model/ViewModel instead.
public class MyViewModel : INotifyPropertyChanged
{
private static MyViewModel s_lastSelectedTestItem = null;
public MyViewModel(string name)
{
Name = name;
m_isSelected = false;
Childs = new ObservableCollection<MyViewModel>();
Childs.CollectionChanged += new NotifyCollectionChangedEventHandler(TestItems_CollectionChanged);
}
void TestItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (IsSelected == true && Childs.Count > 0)
{
Childs[0].IsSelected = true;
IsExpanded = true;
}
}
public string Name
{
get;
set;
}
public ObservableCollection<MyViewModel> Childs
{
get;
set;
}
private bool m_isSelected;
public bool IsSelected
{
get
{
return m_isSelected;
}
set
{
m_isSelected = value;
if (m_isSelected == true)
{
if (s_lastSelectedTestItem != null)
{
s_lastSelectedTestItem.IsSelected = false;
}
s_lastSelectedTestItem = this;
if (Childs.Count > 0)
{
IsExpanded = true;
Childs[0].IsSelected = true;
m_isSelected = false;
}
}
OnPropertyChanged("IsSelected");
}
}
private bool m_isExpaned;
public bool IsExpanded
{
get
{
return m_isExpaned;
}
set
{
m_isExpaned = value;
OnPropertyChanged("IsExpanded");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}

What about to capture select item event on the treeview and expand the first child of the selected item? It seems easy to do.

Related

WPF Type Select list box Query

I am learning WPF and MVVM, and have written a simple Type-Select List box in WPF as a learning practice program.
Though it works,I have three questions which are as under:-
1)How to set the Text property of TxtMail via DataBinding?Currently it messes up with other logic if i set Text property in XAML via DataBinding.I do not want to set the text property of Txtmail directly from Code-Behind,while setting the same via DataBinding in XAML messes up things owing to my limited understanding of the subject.
2)The ItemSource of ListBox named AllMatching is being set from Code-Behind since it is changing programatically with text search patterns.How can i set it from XAML?
3)Is there a way i can remove the logic of GUI Control Events and include the same in XAML?
The entire code is as under:-
ViewModel:
public class VM_Data : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public int p_ID;
public double p_SP, p_CP;
public string p_Name;
public List<DM_Data> AllData;
public List<DM_Data> DynamicData;
public Visibility p_ListVisibility = Visibility.Collapsed;
private DM_Data _currentRec;
public DM_Data CurrentRec
{
get { return _currentRec; }
set { _currentRec = value; RaisePropertyChangedEvent("CurrentRec"); }
}
public VM_Data()
{
LoadData();
}
public int ID
{
get { return p_ID; }
set
{
if (p_ID != value)
{
RaisePropertyChangedEvent("ID");
p_ID = value;
}
}
}
public double SP
{
get { return p_SP; }
set
{
if (p_SP != value)
{
RaisePropertyChangedEvent("SP");
p_SP = value;
}
}
}
public double CP
{
get { return p_CP; }
set
{
if (p_CP != value)
{
RaisePropertyChangedEvent("CP");
p_CP = value;
}
}
}
public Visibility ListVisibility
{
get { return p_ListVisibility; }
set
{
p_ListVisibility = p_ListVisibility != value ? value : p_ListVisibility;
RaisePropertyChangedEvent("ListVisibility");
}
}
public string Name
{
get { return p_Name; }
set
{
if (p_Name != value)
{
RaisePropertyChangedEvent("Name");
p_Name = value;
}
}
}
private void LoadData()
{
AllData = new List<DM_Data>();
DynamicData = new List<DM_Data>();
string[] strNames = "Jatinder;Shashvat;shashikala;shamsher;shahid;justin;jatin;jolly;ajay;ahan;vijay;suresh;namita;nisha;negar;zenith;zan;zen;zutshi;harish;hercules;harman;ramesh;shashank;mandeep;aman;amandeep;amarjit;asim;akshay;amol;ritesh;ritivik;riz;samana;samaira;bhagwandass;bhagwan;bhawna;bhavna".Split(';');
for(int i=0;i<=strNames.GetUpperBound(0);i++)
{
DM_Data NewRec = new DM_Data();
NewRec.CP = new Random().Next(200, 400);
NewRec.SP = new Random().Next(1, 10);
NewRec.ID = i + 1;
NewRec.Name = strNames[i];
AllData.Add(NewRec);
}
AllData = DynamicData = AllData.OrderBy(item => item.Name).ToList();
}
private void RaisePropertyChangedEvent(string Property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(Property));
}
}
}
DataModel:
public class DM_Data
{
public int p_ID;
public double p_SP, p_CP;
public string p_Name;
public Visibility p_ListVisibility = Visibility.Visible;
public int ID
{
get { return p_ID; }
set { p_ID = value; }
}
public Visibility ListVisibility
{
get { return p_ListVisibility; }
set { p_ListVisibility = value; }
}
public double SP
{
get { return p_SP; }
set { p_SP = value; }
}
public double CP
{
get { return p_CP; }
set { p_CP = value; }
}
public string Name
{
get { return p_Name; }
set { p_Name = value; }
}
}
MainWindow.Xaml
<Window x:Class="TypeSelect.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:TypeSelect"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Canvas>
<TextBox x:Name="TxtMail" Width="244" FontSize="14" Canvas.Left="36" Canvas.Top="34" Height="25" GotFocus="TxtMail_GotFocus" LostFocus="TxtMail_LostFocus" KeyUp="TxtMail_KeyUp" MouseUp="TxtMail_MouseUp" />
<ListBox x:Name="AllMatching" Width="{Binding ElementName=TxtMail,Path=Width}" MinHeight="10" MaxHeight="100" Canvas.Top="54" Canvas.Left="36" DisplayMemberPath="Name" SelectedItem="{Binding CurrentRec,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" SelectedValue="Name" SelectedValuePath="Name" Visibility="{Binding ListVisibility,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" KeyUp="AllMatching_KeyUp" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Auto" MouseUp="AllMatching_MouseUp" GotFocus="AllMatching_GotFocus" MouseDoubleClick="AllMatching_MouseDoubleClick" ForceCursor="True">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True" >
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="FontFamily" Value="Arial Bold" />
<Setter Property="Background" Value="LightGray" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="FontSize" Value="18" />
</Trigger>
</Style.Triggers>
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="LightGray"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="LightGray"/>
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="White" />
</Style.Resources>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<Button Content="Test" x:Name="cmdtest" Click="cmdtest_Click"/>
</Canvas>
</StackPanel>
MainWindow.Xaml.cs
namespace TypeSelect
{
public partial class MainWindow : Window
{
VM_Data ViewModel;
DM_Data Existing;
public MainWindow()
{
InitializeComponent();
ViewModel = new VM_Data();
this.DataContext = ViewModel;
AllMatching.ItemsSource = ViewModel.DynamicData;
ViewModel.CurrentRec = Existing= ViewModel.AllData[new Random().Next(0, ViewModel.AllData.Count - 1)];
TxtMail.Text = (ViewModel.CurrentRec != null) ? ViewModel.CurrentRec.Name : "";
}
private void cmdtest_Click(object sender, RoutedEventArgs e)
{
}
private void TxtMail_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key.Equals(Key.Enter))
{
ViewModel.CurrentRec = (DM_Data)AllMatching.SelectedItem;
((TextBox)sender).Text = (ViewModel.CurrentRec != null) ? ViewModel.CurrentRec.Name : "";
//ViewModel.CurrentRec.SearchText = ViewModel.CurrentRec.Name;
ViewModel.ListVisibility = ViewModel.ListVisibility.Equals(Visibility.Collapsed) ? Visibility.Visible : Visibility.Collapsed;
if (ViewModel.ListVisibility.Equals(Visibility.Visible))
{
AllMatching.ScrollIntoView(ViewModel.CurrentRec);
}
else
{
Existing = ViewModel.CurrentRec;
}
}
else if (e.Key.Equals(Key.Escape))
{
ViewModel.CurrentRec = Existing;
ViewModel.ListVisibility = Visibility.Collapsed;
((TextBox)sender).Text = (ViewModel.CurrentRec != null) ? ViewModel.CurrentRec.Name : "";
}
else if (e.Key.Equals(Key.Up))
{
AllMatching.SelectedIndex = AllMatching.SelectedIndex > 0 ? AllMatching.SelectedIndex - 1 : AllMatching.SelectedIndex;
ViewModel.CurrentRec = (DM_Data)AllMatching.SelectedItem;
AllMatching.ScrollIntoView(ViewModel.CurrentRec);
}
else if (e.Key.Equals(Key.Down))
{
AllMatching.SelectedIndex = AllMatching.SelectedIndex < AllMatching.Items.Count ? AllMatching.SelectedIndex + 1 : AllMatching.SelectedIndex;
ViewModel.CurrentRec = (DM_Data)AllMatching.SelectedItem;
AllMatching.ScrollIntoView(ViewModel.CurrentRec);
}
else
{
ViewModel.DynamicData = ViewModel.AllData.Where(item => item.Name.StartsWith(TxtMail.Text, StringComparison.OrdinalIgnoreCase)).ToList<DM_Data>();
AllMatching.ItemsSource = ViewModel.DynamicData;
ViewModel.CurrentRec = (AllMatching.Items.Count > 0) ? ViewModel.DynamicData.Where(item => item.Name.StartsWith(TxtMail.Text, StringComparison.OrdinalIgnoreCase)).FirstOrDefault() : null;
if (AllMatching.Visibility.Equals(Visibility.Collapsed))
{
AllMatching.Visibility = Visibility.Visible;
AllMatching.ScrollIntoView(ViewModel.CurrentRec);
}
}
}
private void TxtMail_GotFocus(object sender, RoutedEventArgs e)
{
DM_Data Existing = ViewModel.CurrentRec;
ViewModel.ListVisibility = Visibility.Visible;
}
private void TxtMail_LostFocus(object sender, RoutedEventArgs e)
{
ViewModel.ListVisibility = Visibility.Collapsed;
}
private void TxtMail_MouseUp(object sender, MouseButtonEventArgs e)
{
ViewModel.ListVisibility = Visibility.Visible;
AllMatching.ScrollIntoView(ViewModel.CurrentRec);
}
private void AllMatching_MouseUp(object sender, MouseButtonEventArgs e)
{
ViewModel.CurrentRec = (DM_Data)AllMatching.SelectedItem;
ViewModel.ListVisibility = Visibility.Visible;
AllMatching.ScrollIntoView(ViewModel.CurrentRec);
}
private void AllMatching_GotFocus(object sender, RoutedEventArgs e)
{
ViewModel.ListVisibility = Visibility.Visible;
}
private void AllMatching_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key.Equals(Key.Enter))
{
TxtMail.Text = ViewModel.CurrentRec.Name;
ViewModel.ListVisibility = Visibility.Collapsed;
}
}
private void AllMatching_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
ViewModel.CurrentRec = (DM_Data)AllMatching.SelectedItem;
TxtMail.Text = (ViewModel.CurrentRec != null) ? ViewModel.CurrentRec.Name : "";
//ViewModel.CurrentRec.SearchText = ViewModel.CurrentRec.Name;
ViewModel.ListVisibility = ViewModel.ListVisibility.Equals(Visibility.Collapsed) ? Visibility.Visible : Visibility.Collapsed;
if (ViewModel.ListVisibility.Equals(Visibility.Visible))
{
AllMatching.ScrollIntoView(ViewModel.CurrentRec);
}
else
{
Existing = ViewModel.CurrentRec;
}
}
}
}
How to set the Text property of TxtMail via DataBinding?
Bind it to a source property of the view model:
<TextBox x:Name="TxtMail" Binding="{Binding Mail}" />
Mail is a property of the VM_Data class, just like Name.
The ItemSource of ListBox named AllMatching is being set from Code-Behind since it is changing programatically with text search patterns. How can i set it from XAML?
You should bind this one as well:
<ListBox x:Name="AllMatching" ItemsSource="{Binding DynamicData}" ...>
For this to work, DynamicData must be a property:
public List<DM_Data> DynamicData { get; set; }
If you are adding items dynamically to it, you should make it an ObservableCollection:
public ObservableCollection<DM_Data> DynamicData { get; set; }
Is there a way i can remove the logic of GUI Control Events and include the same in XAML?
XAML is a markup language. It is not a programming language. But you should look into commands: https://blog.magnusmontin.net/2013/06/30/handling-events-in-an-mvvm-wpf-application/.

WPF listbox with image, how to dynamycally change image on condition

I'm new in WPF and i'm exploring listbox control.
I created a listbox, items represent image plus text.
Xaml code:
<ListBox x:Name="LstB_Checklist" HorizontalAlignment="Left" Height="139" Margin="48,61,0,0" VerticalAlignment="Top" Width="220">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image>
<Image.Style>
<Style TargetType="{x:Type Image}">
<Style.Triggers>
<DataTrigger Binding="{Binding Checked}" Value="false">
<Setter Property="Source" Value="pack://application:,,,/listbox;component/Pictures/BulletOff.png"/>
</DataTrigger>
<DataTrigger Binding="{Binding Checked}" Value="true">
<Setter Property="Source" Value="pack://application:,,,/listbox;component/Pictures/BulletOn.png"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
<TextBlock Text="{Binding Title}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
the binding allows to properly set items and image at startup.
Code:
public MainWindow()
{
InitializeComponent();
List<LstB_Item> items = new List<LstB_Item>();
items.Add(new LstB_Item() { Title = "Item1", Checked = "false" });
items.Add(new LstB_Item() { Title = "Item2", Checked = "false" });
LstB_Checklist.ItemsSource = items;
}
public class LstB_Item
{
public string Title { get; set; }
public string Checked { get; set; }
}
private void Button_Click(object sender, RoutedEventArgs e)
{
//
}
I would like to know how to change the image according to some conditions, when i clik on a button (e.g selected item image turn to "bulletOn" instead of "bulltOff" according to external condition, not based on "onselect" trigger)
Many thanks
As Clemens suggests the class should implement the INotifyPropertyChanged interface and raise change notifications whenever the Checked property is set to a new value:
public class LstB_Item : INotifyPropertyChanged
{
public string Title { get; set; }
private string _checked;
public string Checked
{
get { return _checked; }
set { _checked = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
You should then be able to simply change the value of the Checked property for the item that you want to change the image for:
private void Button_Click(object sender, RoutedEventArgs e)
{
List<LstB_Item> items = LstB_Checklist.ItemsSource as List<LstB_Item>;
items[0].Checked = "true";
}
Please refer to MSDN for more information about the INotifyPropertyChanged interface: https://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged(v=vs.110).aspx
You should also consider changing the type of your Checked property from string to bool:
private bool _checked;
public bool Checked
{
get { return _checked; }
set { _checked = value; NotifyPropertyChanged(); }
}

WPF - MVVM - TreeView - Commands enabled based on selected item

I have found many pages that bear on this in one way or another, but have still not discovered how to achieve it. Here is my XAML:
<TreeView ItemsSource="{Binding Document}" HorizontalAlignment="Stretch" BorderThickness="0" Background="#FFC2A2A2">
<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}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ImageFile}" Height="16" Width="16"/>
<TextBlock Text="{Binding Name}" Margin="5,0,0,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
<MenuItem Header="Delete" Command="{Binding DeleteCommand}"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
I implemented the AddCommand and DeleteCommand based roughly on the Search button implementation in this.
Both commands require the SelectedItem from the tree, so I implemented it in the tree MVVM, added a pointer to the tree MVVM to each item MVVM, and maintain it via the IsSelected property in the item MVVM.
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (value != mIsSelected)
{
mIsSelected = value;
this.OnPropertyChanged("IsSelected");
}
if (mIsSelected)
{
mDocViewModel.SelectedItem = this;
}
}
}
(We use mAbc for data members, rather than _abc.)
This all works. However, the context menus have a context. Based on which is selected, the AddCommand may not be valid, and I want that represented as disabled and enabled in the view.
I put my tests for this condition in the CanExecute method of each command. But at run time, CanExecute seems never to be invoked, and both menu item always appear disabled.
Is there a way to get this done? Is there a simple way?
Thanks,
Art
LATER:
Editing my question appears to be the way to make a longer reply. Here, then, is one of the Command classes ... with respect to the CanExecute mentioned afterwards.
#region DeleteCommand
public ICommand DeleteCommand
{
get { return mDeleteCommand; }
}
void DeleteNode()
{
if (mSelectedItem != null)
{
mSelectedItem.Remove();
mSelectedItem = null;
}
}
private class DeleteNodeCommand : RoutedCommand
{
DocumentRulesViewModel mDocumentViewModel;
public DeleteNodeCommand (DocumentRulesViewModel _docViewModel)
{
mDocumentViewModel = _docViewModel;
}
void SelectedItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
throw new NotImplementedException();
}
public bool CanExecute(object parameter)
{
DesignObjectViewModel current = mDocumentViewModel.SelectedItem;
return (current != null);
}
event EventHandler CanExecuteChanged
{
// I intentionally left these empty because
// this command never raises the event, and
// not using the WeakEvent pattern here can
// cause memory leaks. WeakEvent pattern is
// not simple to implement, so why bother.
add { }
remove { }
}
public void Execute(object parameter)
{
mDocumentViewModel.DeleteNode();
}
public event PropertyChangedEventHandler PropertyChanged;
}
#endregion
I didn't do anything with the event stuff at the bottom, just copied it from an example. And, in that example, the command would always be valid. So maybe the issue lies there.
But I did some prowling for CanExecuteChange, and did not really see what to do with it.
Jim, I guess all I can do it show it all (I'll have to omit the application/model parts, of course.
Main xaml:
<Window x:Class="xDesign.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:xDesign.View"
Title="{StaticResource thisAppName}" Height="350" Width="525">
<DockPanel>
<Menu VerticalAlignment="Top" DockPanel.Dock="Top" BorderThickness="0">
<MenuItem Header="{StaticResource fileMenu}" Name="FileMenu">
<MenuItem Header="{StaticResource newFileMenu}" Click="NewDocumentMenuItem_Click" Name="FileMenuNewDoc"/>
<MenuItem Header="{StaticResource openFileMenu}" Click="OpenDocumentMenuItem_Click" Name="FileMenuOpenDoc" />
<MenuItem Header="{StaticResource closeFileMenu}" Click="CloseDocumentMenuItem_Click" IsEnabled="False" Name="FileMenuCloseDoc" />
<Separator />
<MenuItem Name="FileMenuCheckout" Header="{StaticResource checkoutFileMenu}" Click="FileMenuCheckout_Click"/>
<MenuItem Name="FileMenuCheckin" Header="{StaticResource checkinFileMenu}" Click="FileMenuCheckin_Click" IsEnabled="False"/>
<MenuItem Name="FileMenuDeleteFromServer" Header="{StaticResource deleteFromServerFileMenu}" Click="FileMenuDeleteFromServer_Click" IsEnabled="False"/>
<MenuItem Name="FileMenuLogon" Header="{StaticResource logonFileMenu}" Click="FileMenuLogon_Click"/>
<MenuItem Name="FileMenuLogoff" IsEnabled="False" Header="{StaticResource logoffFileMenu}" Click="FileMenuLogoff_Click"/>
</MenuItem>
<MenuItem Header="{StaticResource editMenu}" IsEnabled="False" Name="EditMenu">
<MenuItem Header="{StaticResource findEditMenu}" Click="FindEditMenuItem_Click"/>
</MenuItem>
<MenuItem Header="{StaticResource viewMenu}" IsEnabled="False" Name="ViewMenu">
<MenuItem Header="{StaticResource expandViewMenu}" Click="ExpandViewMenuItem_Click"/>
<MenuItem Header="{StaticResource collapseViewMenu}" Click="CollapseViewMenuItem_Click"/>
</MenuItem>
</Menu>
<Grid Name="DesignPanel" DockPanel.Dock="Top">
<Grid.ColumnDefinitions >
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions >
<local:DocumentTreeView x:Name="DocTreeView" Grid.Column="0"/>
<GridSplitter Grid.Column="0" HorizontalAlignment="Right" VerticalContentAlignment="Stretch" Width="3" ResizeDirection="Columns" />
<WebBrowser x:Name="objectPreviewBrowser" Grid.Column="1" Margin="6,6,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" OpacityMask="#FF9B8E8E"/>
</Grid>
</DockPanel>
</Window>
Control xaml:
<UserControl x:Class="xDesign.View.DocumentTreeView"
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">
<TreeView ItemsSource="{Binding Document}" HorizontalAlignment="Stretch" BorderThickness="0" Background="#FFC2A2A2">
<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}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ImageFile}" Height="16" Width="16"/>
<TextBlock Text="{Binding Name}" Margin="5,0,0,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add rule" Command="{Binding AddRuleCommand}"/>
<MenuItem Header="Delete" Command="{Binding DeleteCommand}"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
</UserControl>
Primary view model:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
using xDesign.Actions;
using xDesign.API.Model;
namespace xDesign.ViewModel
{
public class DocumentRulesViewModel : INotifyPropertyChanged
{
#region data members
DesignObjectViewModel mRootObject = null;
ObservableCollection<DesignObjectViewModel> mDocument = null;
DesignObjectViewModel mSelectedItem = null;
ICommand mDeleteCommand = null;
ICommand mAddRuleCommand = null;
#endregion
#region consructors
public DocumentRulesViewModel(DocumentObject _rootObject)
{
mRootObject = new DesignObjectViewModel(_rootObject, this);
mDocument = new ObservableCollection<DesignObjectViewModel>
(new DesignObjectViewModel[] { mRootObject });
mRootObject.IsExpanded = true; // We start with the top node expanded
mDeleteCommand = new DeleteNodeCommand(this);
mAddRuleCommand = new AddRuleCommandClass(this);
}
~DocumentRulesViewModel()
{
Close();
}
public void Close()
{
Document = null;
}
#endregion
#region properties
public ObservableCollection<DesignObjectViewModel> Document
{
get { return mDocument; }
set
{
if (value != mDocument)
{
mDocument = value;
this.OnPropertyChanged("Document");
}
}
}
public DesignObjectViewModel SelectedItem
{
get { return mSelectedItem; }
set
{
if (value != mSelectedItem)
{
mSelectedItem = value;
this.OnPropertyChanged("SelectedItem");
}
}
}
public IDesignObject CurrentDesignObject
{
get
{
if (mSelectedItem == null)
{
return null;
}
else
{
return mSelectedItem.DesignObject;
}
}
set
{
DesignObjectViewModel dovm = SearchForNode(value);
if (dovm != null)
{
if (dovm.Parent != null && !dovm.Parent.IsExpanded)
{
dovm.Parent.IsExpanded = true;
}
dovm.IsSelected = true;
}
}
}
#endregion
#region DeleteCommand
public ICommand DeleteCommand
{
get { return mDeleteCommand; }
}
public void DeleteItem ()
{
DesignObjectViewModel node = this.SelectedItem;
node.Remove();
}
private class DeleteNodeCommand : RoutedCommand
{
DocumentRulesViewModel mTree;
public DeleteNodeCommand(DocumentRulesViewModel _tree)
{
mTree = _tree;
}
public bool CanExecute(object parameter)
{
DesignObjectViewModel node = mTree.SelectedItem;
return (node != null);
}
public void Execute(object parameter)
{
mTree.DeleteItem();
}
// allows for constant updating if the event can execute or not.
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public void RaiseCanExecuteChanged()
{
// we should not have to reevaluate every can execute.
// but since there are too many places in product code to verify
// we will settle for all or nothing.
CommandManager.InvalidateRequerySuggested();
}
}
#endregion
#region AddRuleCommand
public ICommand AddRuleCommand
{
get { return mAddRuleCommand; }
}
void AddRule()
{
int index = -1; // Where to insert; -1 = inside selected item
if (mSelectedItem.Parent != null)
{
index = mSelectedItem.Parent.Children.IndexOf(mSelectedItem) + 1; // Insert after selected item
}
// Call the application logic
IDesignObject dobj = DocStructureManagement.AddRule(mSelectedItem.DesignObject, ref index);
if (dobj != null)
{
DesignObjectViewModel newItemParent;
if (index == -1)
{
newItemParent = mSelectedItem;
index = 0;
}
else
{
newItemParent = mSelectedItem.Parent;
}
DesignObjectViewModel newItem = new DesignObjectViewModel(dobj, this, newItemParent);
newItemParent.InsertChild(newItem, index);
}
}
private class AddRuleCommandClass : RoutedCommand
{
DocumentRulesViewModel mTree;
public AddRuleCommandClass(DocumentRulesViewModel _tree)
{
mTree = _tree;
}
public bool CanExecute(object parameter)
{
DesignObjectViewModel node = mTree.SelectedItem;
return (node != null && node.DesignObject.CanContainOrPrecede(eDesignNodeType.ContentRule));
}
public void Execute(object parameter)
{
mTree.AddRule();
}
// allows for constant updating if the event can execute or not.
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public void RaiseCanExecuteChanged()
{
// we should not have to reevaluate every can execute.
// but since there are too many places in product code to verify
// we will settle for all or nothing.
CommandManager.InvalidateRequerySuggested();
}
}
#endregion
#region Search
private DesignObjectViewModel SearchForNode(IDesignObject _dobj)
{
return SearchNodeForNode(mRootObject, _dobj);
}
private DesignObjectViewModel SearchNodeForNode(DesignObjectViewModel _node, IDesignObject _dobj)
{
if (_node.DesignObject == _dobj)
{
return _node;
}
foreach (DesignObjectViewModel child in _node.Children)
{
DesignObjectViewModel childNode = SearchNodeForNode(child, _dobj);
if (childNode != null)
{
return childNode;
}
}
return null;
}
#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 // INotifyPropertyChanged Members
}
}
TreeViewItem view model:
using System;
using System.Collections.ObjectModel;
using System.Linq;
using xDesign.API.Model;
using xDesign.Actions;
using System.ComponentModel;
using System.Windows.Input;
namespace xDesign.ViewModel
{
public class DesignObjectViewModel : INotifyPropertyChanged
{
#region data
DocumentRulesViewModel mDocViewModel = null;
IDesignObject mDesignObject = null;
DesignObjectViewModel mParent = null;
ObservableCollection<DesignObjectViewModel> mChildren = null;
bool mIsSelected = false;
bool mIsExpanded = false;
#endregion
#region constructors
public DesignObjectViewModel(IDesignObject _dobj, DocumentRulesViewModel _docViewModel)
: this(_dobj, _docViewModel, null)
{
}
public DesignObjectViewModel(IDesignObject _dobj, DocumentRulesViewModel _docViewModel, DesignObjectViewModel _parent)
{
mDesignObject = _dobj;
mDocViewModel = _docViewModel;
mParent = _parent;
if (_dobj.Type != eDesignNodeType.ContentGroup)
{
mChildren = new ObservableCollection<DesignObjectViewModel>(
(from child in mDesignObject.Children
select new DesignObjectViewModel(child, mDocViewModel, this))
.ToList<DesignObjectViewModel>());
}
else
{
ContentHolder ch = (ContentHolder)_dobj;
mChildren = new ObservableCollection<DesignObjectViewModel>(
(from child in ch.Contents
select new DesignObjectViewModel(child, mDocViewModel, this))
.ToList<DesignObjectViewModel>());
}
}
#endregion
#region properties
public ObservableCollection<DesignObjectViewModel> Children
{
get { return mChildren; }
}
public DesignObjectViewModel Parent
{
get { return mParent; }
}
public String Name
{
get { return mDesignObject.Name; }
}
public IDesignObject DesignObject
{
get { return mDesignObject; }
}
public Type DataType
{
get { return mDesignObject.GetType(); }
}
// Can we use DataType for this, and task the View with finding a corresponding image?
// And do we want to? We could end up with file names that include Model type names.
// Better? Worse? The same?
public String ImageFile
{
get { return GetImageUri(mDesignObject); }
}
public bool IsExpanded
{
get { return mIsExpanded; }
set
{
if (value != mIsExpanded)
{
mIsExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
if (mIsExpanded && mParent != null)
mParent.IsExpanded = true;
}
}
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (value != mIsSelected)
{
mIsSelected = value;
this.OnPropertyChanged("IsSelected");
if (mIsSelected)
{
mDocViewModel.SelectedItem = this;
}
CommandManager.InvalidateRequerySuggested();
}
}
}
#endregion
#region public methods
public void Remove()
{
DocStructureManagement.DeleteNode(mDesignObject); // Remove from application
if (mParent != null) // Remove from ViewModel
{
mParent.Children.Remove(this);
mParent.OnPropertyChanged("Children");
}
}
public void InsertChild(DesignObjectViewModel _newChild, int _insertIndex)
{
Children.Insert(_insertIndex, _newChild);
this.OnPropertyChanged("Children");
}
#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 // INotifyPropertyChanged Members
internal static string GetImageUri(IDesignObject _dobj)
{
string name = null;
switch (_dobj.Type)
{
case eDesignNodeType.Document:
name = "xDesign.ico";
break;
case eDesignNodeType.ContentRule:
name = "Content Rule.png";
break;
case eDesignNodeType.Section:
name = "section rule.png";
break;
case eDesignNodeType.Table:
name = "Table Rule.bmp";
break;
case eDesignNodeType.Read:
name = "Read Rule.bmp";
break;
case eDesignNodeType.Goto:
name = "Goto Rule.bmp";
break;
case eDesignNodeType.Label:
name = "Label Rule.bmp";
break;
case eDesignNodeType.ContentGroup:
name = "ContentGroup.png";
break;
case eDesignNodeType.Content:
name = "content.png";
break;
case eDesignNodeType.Criteria:
name = "Criteria.bmp";
break;
}
if (name == null)
{
throw new Exception("No image found for " + _dobj.Name);
}
return string.Format(#"C:\DEVPROJECTS\XDMVVM\XDMVVM\Images\{0}", name);
}
}
}
Finally, a code snippet from main window code behind, where I create and connect the main view model.
mDocumentRulesViewModel = new DocumentRulesViewModel(mCurrentDocument);
this.DocTreeView.DataContext = mDocumentRulesViewModel;
Again, I set breakpoints in the CanExecute method of each of the two command classes, and control never stops there.
I created a tiny sample project, similar to yours to solve this. I was able to have the context menu CanExecute behave correctly. If you emulate this style you will be able to solve your problem.
MainWindow.Xaml:
<Window x:Class="CommandChangesInTreeViewContextMenu.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"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button Command="{Binding AddCommand}">Add Command </Button>
<TreeView Grid.Row="1"
ItemsSource="{Binding MasterList}" HorizontalAlignment="Stretch" BorderThickness="0" Background="#FFC2A2A2">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate >
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Margin="5,0,0,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
<!--<MenuItem Header="Delete" Command="{Binding DeleteCommand}"/>-->
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
<Button Grid.Row="2"
Command="{Binding ClearSelectionsCommand}">Clear Selections </Button>
</Grid>
</Window>
The DataContext ViewModel for the MainWindow.Xaml is TreeViewModel:
public class TreeViewModel : ObservableObject
{
private ObservableCollection<MasterItem> _masterList;
private ICommand _addCommand;
private ICommand _clearSelectionsCommand;
public ObservableCollection<MasterItem> MasterList
{
get { return _masterList; }
set
{
if (_masterList != value)
{
_masterList = value;
OnPropertyChanged("MasterList");
}
}
}
public ICommand AddCommand
{
get
{
if (_addCommand == null)
{
_addCommand = new RelayCommand<object>(Add, CanExecuteAddCommand);
}
return _addCommand;
}
}
public ICommand ClearSelectionsCommand
{
get
{
if (_clearSelectionsCommand == null)
{
_clearSelectionsCommand = new RelayCommand<object>(ClearSelections);
}
return _clearSelectionsCommand;
}
}
public TreeViewModel()
{
MasterList = new ObservableCollection<MasterItem>
{
new MasterItem("sup"), new MasterItem("hi"), new MasterItem("test"), new MasterItem("yo")
};
}
private void Add(object o)
{
// does nothing
}
private void ClearSelections(object o)
{
foreach (var mItem in MasterList)
{
mItem.IsSelected = false;
}
}
private bool CanExecuteAddCommand(object o)
{
return MasterList.Any(mItem => mItem.IsSelected == true);
}
}
The MasterItem class which are the objects in your MasterList:
MasterItem.cs:
public class MasterItem : ObservableObject
{
private string _name;
private bool _isSelected;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged("Name");
}
}
}
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
CommandManager.InvalidateRequerySuggested();
}
}
}
public MasterItem(string name)
{
Name = name;
IsSelected = false;
}
}
**Note that When IsSelected is set it will InvalidateRequerySuggested() and work properly. =) **
Supporting Classes, RelayCommand, and ObservableObject
/// <summary>
/// RelayCommand
///
/// General purpose command implementation wrapper. This is an alternative
/// to multiple command classes, it is a single class that encapsulates different
/// business logic using delegates accepted as constructor arguments.
/// </summary>
/// <typeparam name="T"></typeparam>
public class RelayCommand<T> : ICommand
{
private static bool CanExecute(T paramz)
{
return true;
}
private readonly Action<T> _execute;
private readonly Func<T, bool> _canExecute;
/// <summary>
/// Relay Command
///
/// Stores the Action to be executed in the instance field variable. Also Stores the
/// information about IF it canexecute in the instance field variable. These executing
/// commands can be sent from other methods in other classes. Hence the lambda expressions.
/// Tries to be as generic as possible T type as parameter.
/// </summary>
/// <param name="execute">Holds the method body about what it does when it executes</param>
/// <param name="canExecute">Holds the method body conditions about what needs to happen for the ACTION
/// Execute to execute. If it fails it cannot execute. </param>
public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute ?? CanExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute(TranslateParameter(parameter));
}
// allows for constant updating if the event can execute or not.
public event EventHandler CanExecuteChanged
{
add
{
if (_canExecute != null)
CommandManager.RequerySuggested += value;
}
remove
{
if (_canExecute != null)
CommandManager.RequerySuggested -= value;
}
}
public void Execute(object parameter)
{
_execute(TranslateParameter(parameter));
}
private T TranslateParameter(object parameter)
{
T value = default(T);
if (parameter != null && typeof(T).IsEnum)
value = (T)Enum.Parse(typeof(T), (string)parameter);
else
value = (T)parameter;
return value;
}
public void RaiseCanExecuteChanged()
{
// we should not have to reevaluate every can execute.
// but since there are too many places in product code to verify
// we will settle for all or nothing.
CommandManager.InvalidateRequerySuggested();
}
}
/// <summary>
/// Class is based on two delegates; one for executing the command and another for returning the validity of the command.
/// The non-generic version is just a special case for the first, in case the command has no parameter.
/// </summary>
public class RelayCommand : RelayCommand<object>
{
public RelayCommand(Action execute, Func<bool> canExecute = null)
: base(obj => execute(),
(canExecute == null ?
null : new Func<object, bool>(obj => canExecute())))
{
}
}
ObservableObject:
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, Expression<Func<T>> expression)
{
// Allows a comparison for generics. Otherwise could just say x == y ?
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
var lambda = (LambdaExpression)expression;
MemberExpression memberExpr;
if (lambda.Body is UnaryExpression)
{
var unaryExpr = (UnaryExpression)lambda.Body;
memberExpr = (MemberExpression)unaryExpr.Operand;
}
else
{
memberExpr = (MemberExpression)lambda.Body;
}
OnPropertyChanged(memberExpr.Member.Name);
return true;
}
return false;
}
}
Note that ObservableObject and RelayCommand are just helpers and not necessary to generating the solution. Mainly look at MainWindow.Xaml, TreeViewModel, and MasterItem. I hope this helps!
Picture of the disabled context menu when IsSelected is set to false for all the MasterItems in MasterList
Example of using a RelayCommand:
in your constructor
public PrimaryViewModel()
{
ICommand bob = new RelayCommand(CommandMethodThatDoesStuff,CanExecuteCommandMethod);
}
private void CommandMethodThatDoesStuff(object o)
{
// do your work
}
private bool CanExecuteCommandMethod(object o)
{
return IsSelected;
}
You are on the right track with the Commands and implementing CanExecute. I have had a similar problem. The commands will not always update their CanExecute immediately. If you want to have all of your commands update, a simple brute force solution is to add a call to CommandManager.InvalidateRequerySuggested().
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (value != mIsSelected)
{
mIsSelected = value;
this.OnPropertyChanged("IsSelected");
}
if (mIsSelected)
{
mDocViewModel.SelectedItem = this;
CommandManager.InvalidateRequerySuggested();
}
}
}
This will call invalidate arrange on all of your commands and force the logic on your commands, CanExecute boolean methods to refresh their state on the UI.
There are two ways to do this, that I can think of.
1 - put the context menu on your HierarchicalDataTemplate. This means the DataContext for the context menu will be the item from the tree. This can be nice because then, for example, the AddCommand is close to where things need to be added. You don't need to track the selected item in this case.
2 - bind the IsEnabled from the MenuItem to an "IsEnabled" property in your VM, and then update it when the selection changes. This is less nice, but can be tidy. If you are only doing single selection, and you already have a property for the selected item in your VM (which you probably should have already) then you can just bind it to something like {Binding SelectedItem.IsEnabled} or something.
ContextMenus aren't part of the visible tree, so binding directly to the view models won't work. The way around it is to use a BindingProxy as explained on this page:
<TreeView ItemsSource="{Binding Items}" >
<TreeView.Resources>
<local:BindingProxy x:Key="Proxy" Data="{Binding}"/>
</TreeView.Resources>
<TreeView.ContextMenu>
<ContextMenu DataContext="{Binding Path=Data, Source={StaticResource Proxy}}">
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
Alternatively if each tree item has it's own view model then you can add the command handlers to the items themselves and bind relative to the placement target:
<TreeView ItemsSource="{Binding Items}" >
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>

WPF TreeView not being populated

I am trying to dynamically populate a WPF tree by using a ViewModel, however, for some reason it's not working. Either the bindings either aren't properly or I am messing up somewhere in code behind.
Here's a sample of what I have.
In XAML I define my TreeView like so...
<TreeView DockPanel.Dock="Left" Width="200" DataContext="{Binding MessageTree}" ItemsSource="{Binding MessageTree}">
<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 DataType="viewModel:Mail" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Subject}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
In Code Behing i have...
private Mail MessageTree { get; set; }
And
using (var mail = new MailParser())
{
int count = mail.GetMessageCount(DateTime.Today.AddDays(-10), DateTime.Today.AddDays(1));
MessageTree = new Mail();
for (int i = count - 1; i >= 0; i--)
{
MailMessage msg = mail.RetrieveMessage(i);
if (msg != null)
{
MessageTree.Add(msg);
}
if (backgroundWorker != null)
{
decimal perc = (100.0m - (((i + 1.0m)*100.0m)/count));
backgroundWorker.ReportProgress((int) perc, "Recebendo mensagens... " + perc.ToString("N2") + "%");
if (backgroundWorker.CancellationPending)
{
e.Cancel = true;
break;
}
}
}
}
Mail is defined as
public sealed class Mail : INotifyPropertyChanged
{
private readonly ObservableCollection<Mail> _children;
private readonly MailMessage _msg;
private readonly Mail _parent;
private bool _isExpanded;
private bool _isSelected;
public Mail()
{
_msg = new MailMessage {Subject = "Empty"};
_parent = null;
_children = new ObservableCollection<Mail>();
}
public Mail(MailMessage msg, Mail parent = null)
{
_msg = msg;
_parent = parent;
_children = new ObservableCollection<Mail>();
}
public IEnumerable<Mail> Children
{
get { return _children; }
}
public string Subject
{
get { return _msg.Subject; }
}
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != _isExpanded)
{
_isExpanded = value;
OnPropertyChanged();
}
if (_isExpanded && _parent != null)
_parent.IsExpanded = true;
}
}
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void Add(MailMessage msg)
{
_children.Add(new Mail(msg, this));
OnPropertyChanged("Children");
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I can't find anything in it so different from examples found online that it wouldn't work. The Add method is incomplete, I still need some logic to decide whether to add them to the collection or to the collection of one of the collection members, but as is all my Mail objecys are beeing added to the collection but not showing up in the TreeView.
What totally obvious thing am i missing? Shouldn't the TreeView automaticly update as I add items to the collection?
What I want is for the TreeView to show The children of the MessageTree property, and those children's children.
EDIT: Couldn't see the whole thing on my phone - amended answer based on ability to actually see everything. :)
MOREEDIT: updated based on comments, let's start from scratch!
First off, if you're set on using the window/whatever as the datacontext, let's make it `INotifyPropertyChange...next, let's make "MessageTree" a collection of mails, not just a single one (it'll make binding semantics easier, trust me)
public class WhateverContainsTheTree : Window, INotifyPropertyChanged
{
public WhateverContainsTheTree()
{
this.Loaded += OnLoaded;
this._messageTree = new ObservableCollection<Mail>();
this.DataContext = this;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
_worker = new BackgroundWorker();
_worker.DoWork += WorkerWorkin;
_worker.RunWorkerAsync();
}
private BackgroundWorker _worker;
private ObservableCollection<Mail> _messageTree;
public ObservableCollection<Mail> MessageTree
{
get { return _messageTree; }
set { _messageTree = value; RaisePropertyChanged("MessageTree"); }
}
public event PropertyChangedEventHandler PropertyChanged = delegate {};
private void RaisePropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private void WorkerWorkin(object sender, DoWorkEventArgs e)
{
// obviously, change this to your stuff; I added a ctor so I could pass a string
Thread.Sleep(3000);
Console.WriteLine("Ok, setting message tree");
Dispatcher.Invoke(
System.Windows.Threading.DispatcherPriority.Normal,
(Action)(() =>
{
var mail1 = new Mail("Mail#1:Changed from background thread");
var mail2 = new Mail("Mail#2:Submail of mail #1");
var mail3 = new Mail("Mail#3:Submail of mail #2");
var mail4 = new Mail("Mail#4:Submail of mail #1");
var mail5 = new Mail("Mail#5:Submail of mail #4");
mail1.Children.Add(mail2);
mail1.Children.Add(mail4);
mail2.Children.Add(mail3);
mail4.Children.Add(mail5);
MessageTree.Add(mail1);
})
);
}
}
Also, like I'd said in the original response, let's slightly tweak Mail.Children:
public ObservableCollection<Mail> Children
{
get { return _children; }
}
And here's what I used for the treeview xaml:
<TreeView DockPanel.Dock="Left" Width="200" ItemsSource="{{Binding MessageTree}}">
<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 DataType="viewModel:Mail" ItemsSource="{{Binding Children}}">
<TextBlock Text="{{Binding Subject}}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
If this STILL doesn't work, I'll just paste in the whole LINQPad blob I put together to test this.
Without seeing the entire setup, I'm not positive but my guess would be that since MessageTree is a plain CLR property (rather than something that raises PropertyChanged or a DependencyProperty or something, that the binding is occurring before your MessageTree = new Mail(); call. When you set it to a new instance, the binding system isn't getting notified since it is a plain property.
Another potential issue is that you say that code is in the code-behind. Just using that Binding syntax won't pick up a property from the code-behind. It's possible that you're setting that up somewhere else in the code that you didn't show us. But generally you aren't going to be binding from the View to the code-behind, you'd be binding to a ViewModel that was used as the DataContext for the view itself.
Had to give a name to the TreeView (Tree) and then after
MessageTree = new Mail();
insert
Tree.ItemSource = MessageTree.Children;
I find this ugly but at least it works now. Thank you all for trying to help.

Synchronizing a SelectedPath property with the SelectedItem in WPF's TreeView

I am trying to create a SelectedPath property (e.g. in my view-model) that is synchronized with a WPF TreeView. The theory is as follows:
Whenever the selected item in the tree view is changed (SelectedItem property/SelectedItemChanged event), update the SelectedPath property to store a string that represents the whole path to the selected tree node.
Whenever the SelectedPath property is changed, find the tree node indicated by the path string, expand the whole path to that tree node, and select it, after de-selecting the previously selected node.
In order to make all of this reproducible, let us assume that all tree nodes are of type DataNode (see below), that every tree node has a name that is unique among the children of its parent node, and that the path separator be a single forward slash /.
Updating the SelectedPath property in the SelectedItemChange event is not a problem - the following event handler works flawlessly:
void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
DataNode selNode = e.NewValue as DataNode;
if (selNode == null) {
vm.SelectedPath = null;
} else {
vm.SelectedPath = selNode.FullPath;
}
}
However, I fail to make the other way round work properly. Hence, my question, based on the generalized and minimized code sample below, is: How do I make WPF's TreeView respect my programmatical selection of items?
Now, how far have I come? First of all, TreeView's SelectedItem property is read-only, so it cannot be set directly. I have found and read numerous SO questions discussing this in-depth (such as this, this or this), and also resources on other sites, such as this blogpost, this article or this blogpost.
Almost all of these resources point to defining a style for TreeViewItem that binds TreeViewItem's IsSelected property to an equivalent property of the underlying tree node object from the view-model. Sometimes (e.g. here and here), the binding is made two-way, sometimes (e.g. here and here) it's a one-way binding. I don't see the point in making this a one-way-binding (if the tree view UI somehow deselects the item, that change should of course be reflected in the underlying view-model), so I have implemented the two-way version. (The same is usually suggested for IsExpanded, so I have also added a property for that.)
This is the TreeViewItem style I'm using:
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
I have confirmed that this style is actually applied (if I add a setter to set the Background property to Red, all the tree view items do appear with a red background).
And here is the simplified and generalized DataNode class:
public class DataNode : INotifyPropertyChanged
{
public DataNode(DataNode parent, string name)
{
this.parent = parent;
this.name = name;
}
private readonly DataNode parent;
private readonly string name;
public string Name {
get {
return name;
}
}
public override string ToString()
{
return name;
}
public string FullPath {
get {
if (parent != null) {
return parent.FullPath + "/" + name;
} else {
return "/" + name;
}
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null) {
PropertyChanged(this, e);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private DataNode[] children;
public IEnumerable<DataNode> Children {
get {
if (children == null) {
children = DataSource.GetChildNodes(FullPath).Select(s => new DataNode(this, s)).ToArray();
}
return children;
}
}
private bool isSelected;
public bool IsSelected {
get {
return isSelected;
}
set {
if (isSelected != value) {
isSelected = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsSelected"));
}
}
}
private bool isExpanded;
public bool IsExpanded {
get {
return isExpanded;
}
set {
if (isExpanded != value) {
isExpanded = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsExpanded"));
}
}
}
public void ExpandPath()
{
if (parent != null) {
parent.ExpandPath();
}
IsExpanded = true;
}
}
As you can see, each node has a name, a reference to its parent node (if any), it initializes its child nodes lazily, but only once, and it has an IsSelected and an IsExpanded property, both of which trigger the PropertyChanged event from the INotifyPropertyChanged interface.
So, in my view-model, the SelectedPath property is implemented as follows:
public string SelectedPath {
get {
return selectedPath;
}
set {
if (selectedPath != value) {
DataNode prevSel = NodeByPath(selectedPath);
if (prevSel != null) {
prevSel.IsSelected = false;
}
selectedPath = value;
DataNode newSel = NodeByPath(selectedPath);
if (newSel != null) {
newSel.ExpandPath();
newSel.IsSelected = true;
}
OnPropertyChanged(new PropertyChangedEventArgs("SelectedPath"));
}
}
}
The NodeByPath method correctly (I've checked this) retrieves the DataNode instance for any given path string. Nonetheless, I can run my application and see the following behavior, when binding a TextBox to the SelectedPath property of the view-model:
type /0 => item /0 is selected and expanded
type /0/1/2 => item /0 remains selected, but item /0/1/2 gets expanded.
Similarly, when I first set the selected path to /0/1, that item gets correctly selected and expanded, but for any subsequent path values, the items only get expanded, never selected.
After debugging for a while, I thought the problem was a recursive call of the SelectedPath setter in the prevSel.IsSelected = false; line, but adding a flag that would prevent the execution of the setter code while that command is being executed did not seem to change the behaviour of the programme at all.
So, what am I doing wrong here? I don't see where I'm doing something different than what is suggested in all of those blogposts. Does the TreeView need to be notified somehow about the new IsSelected value of the newly selected item?
For your convencience, the full code of all 5 files that constitute the self-contained, minimal example (the data source obviously returns bogus data in this example, yet it returns a constant tree and hence makes the test cases indicated above reproducible):
DataNode.cs
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
namespace TreeViewTest
{
public class DataNode : INotifyPropertyChanged
{
public DataNode(DataNode parent, string name)
{
this.parent = parent;
this.name = name;
}
private readonly DataNode parent;
private readonly string name;
public string Name {
get {
return name;
}
}
public override string ToString()
{
return name;
}
public string FullPath {
get {
if (parent != null) {
return parent.FullPath + "/" + name;
} else {
return "/" + name;
}
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null) {
PropertyChanged(this, e);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private DataNode[] children;
public IEnumerable<DataNode> Children {
get {
if (children == null) {
children = DataSource.GetChildNodes(FullPath).Select(s => new DataNode(this, s)).ToArray();
}
return children;
}
}
private bool isSelected;
public bool IsSelected {
get {
return isSelected;
}
set {
if (isSelected != value) {
isSelected = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsSelected"));
}
}
}
private bool isExpanded;
public bool IsExpanded {
get {
return isExpanded;
}
set {
if (isExpanded != value) {
isExpanded = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsExpanded"));
}
}
}
public void ExpandPath()
{
if (parent != null) {
parent.ExpandPath();
}
IsExpanded = true;
}
}
}
DataSource.cs
using System;
using System.Collections.Generic;
namespace TreeViewTest
{
public static class DataSource
{
public static IEnumerable<string> GetChildNodes(string path)
{
if (path.Length < 40) {
for (int i = 0; i < path.Length + 2; i++) {
yield return (2 * i).ToString();
yield return (2 * i + 1).ToString();
}
}
}
}
}
ViewModel.cs
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
namespace TreeViewTest
{
public class ViewModel : INotifyPropertyChanged
{
private readonly DataNode[] rootNodes = DataSource.GetChildNodes("").Select(s => new DataNode(null, s)).ToArray();
public IEnumerable<DataNode> RootNodes {
get {
return rootNodes;
}
}
private DataNode NodeByPath(string path)
{
if (path == null) {
return null;
} else {
string[] levels = selectedPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
IEnumerable<DataNode> currentAvailable = rootNodes;
for (int i = 0; i < levels.Length; i++) {
string node = levels[i];
foreach (DataNode next in currentAvailable) {
if (next.Name == node) {
if (i == levels.Length - 1) {
return next;
} else {
currentAvailable = next.Children;
}
break;
}
}
}
return null;
}
}
private string selectedPath;
public string SelectedPath {
get {
return selectedPath;
}
set {
if (selectedPath != value) {
DataNode prevSel = NodeByPath(selectedPath);
if (prevSel != null) {
prevSel.IsSelected = false;
}
selectedPath = value;
DataNode newSel = NodeByPath(selectedPath);
if (newSel != null) {
newSel.ExpandPath();
newSel.IsSelected = true;
}
OnPropertyChanged(new PropertyChangedEventArgs("SelectedPath"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null) {
PropertyChanged(this, e);
}
}
}
}
Window1.xaml
<Window x:Class="TreeViewTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TreeViewTest" Height="450" Width="600"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TreeView ItemsSource="{Binding RootNodes}" SelectedItemChanged="TreeView_SelectedItemChanged">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding .}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<TextBox Grid.Row="1" Text="{Binding SelectedPath, Mode=TwoWay}"/>
</Grid>
</Window>
Window1.xaml.cs
using System;
using System.Windows;
namespace TreeViewTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = vm;
}
void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
DataNode selNode = e.NewValue as DataNode;
if (selNode == null) {
vm.SelectedPath = null;
} else {
vm.SelectedPath = selNode.FullPath;
}
}
private readonly ViewModel vm = new ViewModel();
}
}
I could not reproduce the behavior you described. There is a problem with the code you posted unrelated to TreeView. The TextBox default UpdateSourceTrigger is LostFocus therefore the TreeView is affected only after the TextBox loses focus but there are only two controls in your example so to make the TextBox lose focus you have to select something in the TreeView (then the entire selection process is messed up).
What I did was to add a button at the bottom of the form. The button does nothing but when clicked the TextBox loses focus. Everything works perfectly now.
I compiled it in VS2012 using .Net 4.5

Resources