My Node class:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
namespace FrontEnd
{
public enum NodeType
{
SQLite,
Database,
TableCollection,
ViewCollection,
IndexCollection,
TriggerCollection,
ColumnCollection,
Table,
View,
Column,
Index,
Trigger
}
public class Node
{
public string Title { get; protected set; }
public NodeType Type { get; protected set; }
public ObservableCollection<Node> Nodes { get; set; }
public Node(string title, NodeType type)
{
this.Title = title;
this.Type = type;
this.Nodes = new ObservableCollection<Node>();
}
}
}
My XAML:
<TreeView Name="dbTree" Padding="0,5,0,0">
<TreeView.Resources>
<ContextMenu x:Key="ScaleCollectionPopup">
<MenuItem Header="New Scale..."/>
</ContextMenu>
<ContextMenu x:Key="ScaleItemPopup">
<MenuItem Header="Remove Scale"/>
</ContextMenu>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Type, RelativeSource={RelativeSource Self}}" Value="NodeType.Column">
<Setter Property="ContextMenu" Value="{StaticResource ScaleItemPopup}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Nodes}">
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<Image Source="{Binding Converter={StaticResource StringToImageConverter}}" />
<TextBlock Text="{Binding Title}" Padding="5,0,0,0" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
What I am trying to achieve and failing is to decide on the ContextMenu to use based on the Type property of the bound Node classes.
If its a Table or View I would like to display "SELECT 1000 ROWS" & "SHOW CREATE SQL", for other types I want to define other options.
What is the correct way to achieve the desired effect?
I prefer to do it in mvvm style when the context menu is generated by view model of each node. See the example below:
View part:
<Window x:Class="WpfApplication1.MainWindow"
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"
xmlns:wpfApplication1="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
d:DataContext="{d:DesignInstance wpfApplication1:ViewModel}">
<Grid>
<TreeView ItemsSource="{Binding Path=Nodes}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Nodes}">
<StackPanel>
<StackPanel.ContextMenu>
<ContextMenu ItemsSource="{Binding ContextMenu}">
<ContextMenu.Resources>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Command}"/>
</Style>
</ContextMenu.Resources>
<ContextMenu.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Title}"/>
</HierarchicalDataTemplate>
</ContextMenu.ItemTemplate>
</ContextMenu>
</StackPanel.ContextMenu>
<TextBlock Text="{Binding Title}"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
And view model part:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
public class MenuItem
{
public MenuItem()
{
Items = new ObservableCollection<MenuItem>();
}
public string Title { get; set; }
public ICommand Command { get; set; }
public ObservableCollection<MenuItem> Items { get; private set; }
}
public class ViewModel
{
public ViewModel()
{
Nodes = new ObservableCollection<Node>
{
new Node("MSSQL", NodeType.Database,
new Node("Customers", NodeType.Table)),
new Node("Oracle", NodeType.Database)
};
}
public ObservableCollection<Node> Nodes { get; set; }
}
public enum NodeType
{
Database,
Table,
}
public class Node
{
public string Title { get; protected set; }
public NodeType Type { get; protected set; }
public ObservableCollection<Node> Nodes { get; set; }
public Node(string title, NodeType type, params Node[] nodes)
{
this.Title = title;
this.Type = type;
this.Nodes = new ObservableCollection<Node>();
if (nodes != null)
nodes.ToList().ForEach(this.Nodes.Add);
}
public IEnumerable<MenuItem> ContextMenu
{
get { return createMenu(this); }
}
private static IEnumerable<MenuItem> createMenu(Node node)
{
switch (node.Type)
{
case NodeType.Database:
return new List<MenuItem>
{
new MenuItem {Title = "Create table...", Command = new RelayCommand(o => MessageBox.Show("Table created"))}
};
case NodeType.Table:
return new List<MenuItem>
{
new MenuItem {Title = "Select..."},
new MenuItem {Title = "Edit..."}
};
default:
return null;
}
}
}
public class RelayCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
[DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion // ICommand Members
}
(you can use any implementation ICommand interface, RelayCommand is one of them)
You can generate menu items in the Node class or in an IContextMenuBuilder service that can be passed into the Node constructor.
Related
Goal
My actual goal is to navigate the ContentPresenter not by the main navigation, but via a button within the navigated page.
My current results
This is my Main navigation on the left hand side:
When clicked on either of the main navigation items, the ContentPresenter will load it's ViewModel.
Here is the Home tab
and the Some Other tab
Expected results
My expectation is to click on the button (See image below) from the loaded View Model, and navigate to the other view model...
But I am not sure how to implement such idea.
Code
Page View Model
public class PageViewModel
{
public string Title { get; set; }
public object Content { get; set; }
public List<PageViewModel> Children { get; set; }
}
Main View Model
public class MainViewModel
{
public List<PageViewModel> Navigation { get; set; }
public MainViewModel()
{
Navigation = new List<PageViewModel>
{
new PageViewModel
{
Title = "Home",
Content = new HomeViewModel()
},
new PageViewModel
{
Title = "Some Other Tab",
Content = new SomeOtherViewModel()
}
};
}
}
MainWindow.xaml
...
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel>
<ListView ItemsSource="{Binding Navigation}"
x:Name="Nav">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:PageViewModel}">
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
<ContentPresenter Content="{Binding ElementName=Nav, Path=SelectedItem.Content}"
Grid.Column="1"/>
</Grid>
</Window>
Home View Model
public class HomeViewModel
{
public string SomeTitle { get; set; }
public HomeViewModel()
{
SomeTitle = "Hello Home ViewModel";
}
}
Some Other View Model
public class SomeOtherViewModel
{
public string SomeTitle { get; set; }
public SomeOtherViewModel()
{
SomeTitle = "Hello SomeOther View Model";
}
}
Question
What would be the correct implementation to navigate via the internal (child) view model?
You must implement INotifyPropertyChanged in the MainViewModel and add a property called SelectedItem to bind to the listview.
I put the code to do this below. The code works properly.
PageViewModel.cs
public class PageViewModel
{
public string Title { get; set; }
public object Content { get; set; }
}
MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
public List<PageViewModel> Navigation { get; set; }
private PageViewModel selectedItem { get; set; }
public PageViewModel SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
public MainViewModel()
{
Navigation = new List<PageViewModel>
{
new PageViewModel
{
Title = "Home",
Content = new HomeViewModel(this),
},
new PageViewModel
{
Title = "Some Other Tab",
Content = new SomeOtherViewModel(),
}
};
SelectedItem = Navigation.FirstOrDefault();
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
HomeViewModel.cs
public class HomeViewModel
{
public string SomeTitle { get; set; }
public object View { get; set; }
MainViewModel mainViewModel;
public RelayCommand SomeOtherCommand { get; private set; }
public HomeViewModel(MainViewModel _mainViewModel)
{
SomeTitle = "Hello Home ViewModel";
View = new View1(this);
mainViewModel = _mainViewModel;
SomeOtherCommand = new RelayCommand(SomeOtherMethod);
}
private void SomeOtherMethod(object parameter)
{
mainViewModel.SelectedItem = mainViewModel.Navigation.Where(a => a.Title == "Some Other Tab").FirstOrDefault();
}
}
SomeOtherViewModel.cs
public class SomeOtherViewModel
{
public string SomeTitle { get; set; }
public object View { get; set; }
public SomeOtherViewModel()
{
SomeTitle = "Hello SomeOther View Model";
View = new View2();
}
}
RelayCommand.cs
public class RelayCommand : ICommand
{
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new NullReferenceException("execute");
}
else
{
_execute = execute;
_canExecute = canExecute;
}
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}
public RelayCommand(Action<object> execute) : this(execute, null)
{
}
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute.Invoke(parameter);
}
}
MainWindow.xaml
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel>
<ListView ItemsSource="{Binding Navigation}" x:Name="Nav" SelectedItem="{Binding Path=SelectedItem,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:PageViewModel}">
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
<ContentPresenter Content="{Binding ElementName=Nav, Path=SelectedItem.Content.View}" Grid.Column="1" />
</Grid>
View1.xaml
<Grid>
<Button x:Name="btnGet" Content="get" Height="40" Command="{Binding SomeOtherCommand}"></Button>
</Grid>
View2.xaml
<Grid>
<Button Content="test"></Button>
</Grid>
View1.cs
public partial class View1 : UserControl
{
public View1(HomeViewModel homeViewModel)
{
InitializeComponent();
DataContext = homeViewModel;
}
}
How do I update the source of a ContentControl's Content Binding?
<ContentControl Content="{Binding ViewModel.SelectedType, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, Converter={local:TypeMappingConverter}, Mode=TwoWay}" />
I'm going to write an application, where on run time I want to annotate C#-Types and save this to a file and reload it. This is a Screenshot of my UI:
On left the User can select from the available Types and on the right the information of the selected Type are shown through a ContentControl.
This is my Model class:
public class TypeMapping
{
public Type MappedType
{
get => Type.GetType(MappedTypeName);
set => MappedTypeName = value.FullName;
}
public string MappedTypeName { get; set; }
public IEnumerable<PropertyMapping> MappedProperties { get; set; } = Array.Empty<PropertyMapping>();
public virtual string SomeText { get; set; }
}
I only want to store the mapped properties but not all available properties. So my TypeMapping is converted to a TypeMappingViewModel:
public class TypeMappingViewModel : TypeMapping, INotifyPropertyChanged
{
public IEnumerable<PropertyMapping> AvailableProperties { get; set; }
public override string SomeText
{
get => base.SomeText;
set { base.SomeText = value; NotifyPropertyChanged(); }
}
public TypeMappingViewModel(TypeMapping from)
{
MappedTypeName = from.MappedTypeName;
MappedProperties = from.MappedProperties;
AvailableProperties = MappedType.GetProperties().Select(pi => new PropertyMapping { PropertyName = pi.Name });
SomeText = from.SomeText;
}
public TypeMapping ToTypeMapping()
{
return new TypeMapping
{
MappedProperties = MappedProperties,
MappedTypeName = MappedTypeName,
SomeText = SomeText
};
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
This is all other classes:
public class MainViewModel
{
public ObservableCollection<TypeMapping> MappedTypes { get; set; }
= new ObservableCollection<TypeMapping>(new[]
{
new TypeMapping { MappedTypeName = "System.Threading.Tasks.Task" },
new TypeMapping { MappedTypeName = "System.Type" }
});
public TypeMapping SelectedType { get; set; }
}
public class PropertyMapping
{
public string PropertyName { get; set; }
public string SomeText { get; set; }
}
public partial class MainWindow : Window
{
public MainViewModel ViewModel { get; } = new MainViewModel();
public MainWindow()
{
InitializeComponent();
}
}
public class TypeMappingConverter : MarkupExtension, IValueConverter
{
#region IValueConverter
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is TypeMapping typeMapping)
return new TypeMappingViewModel(typeMapping);
else
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is TypeMappingViewModel typeMappingViewModel)
return typeMappingViewModel.ToTypeMapping();
else
return value;
}
#endregion
#region MarkupExtension
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
#endregion
}
and the XAML:
<Window x:Class="ListViewHowTo.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:ListViewHowTo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style TargetType="FrameworkElement" x:Key="baseStyle">
<Setter Property="Margin" Value="3" />
</Style>
</Window.Resources>
<DockPanel>
<ListBox ItemsSource="{Binding ViewModel.MappedTypes, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}"
SelectedItem="{Binding ViewModel.SelectedType, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}"
DisplayMemberPath="MappedTypeName"
Style="{StaticResource baseStyle}"/>
<ContentControl Content="{Binding ViewModel.SelectedType, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}, Converter={local:TypeMappingConverter}, Mode=TwoWay}">
<ContentControl.ContentTemplate>
<DataTemplate DataType="{x:Type local:TypeMappingViewModel}">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding MappedTypeName}" Style="{StaticResource baseStyle}" />
<TextBox Text="{Binding SomeText}" Style="{StaticResource baseStyle}" />
<ListView ItemsSource="{Binding AvailableProperties}" Style="{StaticResource baseStyle}">
<ListView.View>
<GridView>
<GridViewColumn Header="PropertyName" DisplayMemberBinding="{Binding PropertyName}" />
<GridViewColumn Header="SomeText" DisplayMemberBinding="{Binding SomeText}" />
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</DockPanel>
</Window>
The value is properly converted. But how do I fire ConvertBack? Otherwise my entered Text is lost after changing the selected Type.
I need the ContentControl, because later there will be different types of mapping with different views, which I want to select via TemplateSelector.
The ItemsSource of the ListBox should be bound to a collection of view model instances instead of a collection of model instances that need to be converted. The conversion is not necessary at all.
public class MainViewModel
{
public ObservableCollection<TypeMappingViewModel> MappedTypes { get; }
= new ObservableCollection<TypeMappingViewModel>
{
new TypeMappingViewModel { MappedTypeName = "System.Threading.Tasks.Task" },
new TypeMappingViewModel { MappedTypeName = "System.Type" }
};
public TypeMappingViewModel SelectedType { get; set; }
}
With
DataContext = ViewModel;
in the MainWindow constructor, you would bind the Content like this, and entering text into the TextBlock would directly set the SomeText property of the appropriate view model item:
<ContentControl Content="{Binding SelectedType}">
...
<TextBox Text="{Binding SomeText}" />
...
</ContentControl>
I was able to implement the TreeView successfully and I am using an Interface as the template to display the data for Project/Folder/File just like SolutionExplorer of Visual Studio. I want to add contextmenu to my TreeViewItem based on the item type as described in my xaml file below. But I was not able to map the contextmenu based on the TreeViewItem. I tried to see if I can use Triggers which works but I don't know how to assign the ContextMenu based on the TreeViewItem types(e.g. Folder/File/Project in my case).
<!--<Trigger Property="IsSelected" Value="Folder">
<Setter Property="ContextMenu" Value="{StaticResource FolderMenu}" />
</Trigger>-->
<Window x:Class="SimpleTreeWpfApplication1.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:SimpleTreeWpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:TreeViewModel/>
</Window.DataContext>
<Grid>
<TextBlock Text="Hierarchical root binding" Foreground="Red" Margin="10,20,0,0"/>
<TreeView ItemsSource="{Binding TreeData}" Margin="10" Height="200">
<TreeView.Resources>
<!-- Begin Context Menu -->
<ContextMenu x:Key="ProjectMenu" >
<MenuItem Command="{Binding AddFolder}" Header="Add Folder"/>
<MenuItem Command="{Binding EditProject}" Header="Edit"/>
</ContextMenu>
<ContextMenu x:Key="FolderMenu" >
<MenuItem Command="{Binding AddFolder}" Header="Add Folder"/>
<MenuItem Command="{Binding AddFile}" Header="Add File"/>
</ContextMenu>
<ContextMenu x:Key="FileMenu" >
<MenuItem Command="{Binding EditFile}" Header="Edit"/>
</ContextMenu>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="ContextMenu" Value="{StaticResource FolderMenu}"/>
<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}" DataType="{x:Type local:INode}">
<TreeViewItem Header="{Binding Label}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
The interface and models look like below:
public interface INode
{
string FullPath { get; }
string Label { get; }
List<INode> Children { get; }
}
public class Folder : INode
{
public List<INode> Children { get; set; }
public string Label { get; set; }
public string FullPath { get; set; }
//custom values
public Dictionary<string,string> MyFolderProperties { get; set; }
//initialize default values
public Folder()
{
Children = new List<INode>();
}
}
public class File : INode
{
public List<INode> Children { get; set; }
public string Label { get; set; }
public string FullPath { get; set; }
//custom values
public Dictionary<string,string> MyFileProperties { get; set; }
}
public class Project: INode
{
public List<INode> Children { get; set; }
public string Label { get; set; }
public string FullPath { get; set; }
public List<String> ProjectTypeValuesDb { get; set; }
//initialize default values
public Project()
{
Children = new List<INode>();
}
}
//Viewmodel code
public class TreeViewModel : INotifyPropertyChanged
{
public TreeViewModel()
{
//initialize and add
m_folders = new List<INode>();
TreeData = m_folders;
//add Root items
TreeData.Add(new Folder { Label = "Folder1", FullPath = #"C:\dummy1" });
TreeData.Add(new Folder { Label = "Folder2", FullPath = #"C:\dummy2" });
TreeData.Add(new Folder { Label = "Folder3", FullPath = #"C:\dummy3" });
TreeData.Add(new Folder { Label = "Folder4", FullPath = #"C:\dummy4" });
//Folders.Add(new File { Label = "File1.txt", FullPath = #"C:\File1.txt" });
//add sub items
TreeData[0].Children.Add(new Folder { Label = "Folder11", FullPath = #"C:\dummy11" });
TreeData[0].Children.Add(new Folder { Label = "Folder12", FullPath = #"C:\dummy12" });
TreeData[0].Children.Add(new Folder { Label = "Folder13", FullPath = #"C:\dummy13" });
TreeData[0].Children.Add(new Folder { Label = "Folder14", FullPath = #"C:\dummy14" });
TreeData[0].Children.Add(new File { Label = "File1.txt", FullPath = #"C:\File1.txt" });
TreeData[0].Children.Add(new File { Label = "File2.txt", FullPath = #"C:\File1.txt" });
TreeData[0].Children.Add(new File { Label = "File3.txt", FullPath = #"C:\File1.txt" });
}
bool _isExpanded = false;
bool _isSelected = false;
#region IsExpanded
/// <summary>
/// Gets/sets whether the TreeViewItem
/// </summary>
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != _isExpanded)
{
_isExpanded = value;
this.NotifiyPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
//if (_isExpanded && _parent != null)
// _parent.IsExpanded = true;
}
}
#endregion // IsExpanded
#region IsSelected
/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is selected.
/// </summary>
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
this.NotifiyPropertyChanged("IsSelected");
}
}
}
#endregion // IsSelected
private List<INode> m_folders;
public List<INode> TreeData
{
get { return m_folders; }
set
{
m_folders = value;
NotifiyPropertyChanged("Folders");
}
}
void NotifiyPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler PropertyChanged;
}
That's a pretty simple HierarchicalDataTemplate. Just clone it for DataType="{x:Type local:Folder}", DataType="{x:Type local:File}", etc., and provide the appropriate ContextMenu to the TreeViewItem in each template. You'd define the templates in TreeView.Resources instead of in the ItemTemplate property.
I have this TreeView control:
<TreeView x:Name="treeView" HorizontalAlignment="Left" Margin="0" Width="300" ItemsSource="{Binding TvMview.FirstNodes}"
VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding SecondNodes}">
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding ThirdNodes}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding HeaderText}" Background="Red"/>
</StackPanel>
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text=">>> " Background="Green"/>
<TextBlock Text="{Binding HeaderText}" Background="blue"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
<Setter Property="IsExpanded" Value="{Binding IsExpanded}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
Binded to:
public class ModelView
{
public TreeViewModelView TvMview { get; set; } = new TreeViewModelView();
}
public class TreeViewModelView
{
public ObservableCollection<FirstNode> FirstNodes { get; set; } = new ObservableCollection<FirstNode>();
}
public class FirstNode : TreeItem
{
public ObservableCollection<SecondNode> SecondNodes { get; set; } = new ObservableCollection<SecondNode>();
public string HeaderText { get; set; }
}
public class SecondNode : TreeItem
{
public ObservableCollection<ThirdNode> ThirdNodes { get; set; } = new ObservableCollection<ThirdNode>();
public string HeaderText { get; set; }
}
public class ThirdNode
{
public string HeaderText { get; set; }
}
public class TreeItem
{
public bool IsExpanded { get; set; }
public bool IsSelected { get; set; }
}
And everything is working as expected. But... I want to implement seach:
foreach (var firstNode in Mview.TvMview.FirstNodes)
{
if (firstNode.HeaderText.Contains(textBox.Text))
{
firstNode.IsExpanded = true;
firstNode.IsSelected = true;
TreeViewItem tvItem = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(firstNode);
tvItem?.Focus();
return;
}
}
The right node is expanded but tvItem is always null. I read somewhere sometime that ContainerFromItem doesn't likes virtualization...
Any idea?
Thanks!
Finally i solved this issue implementing a Focus() method in the TreeItem base class. So, base class look like this:
public class TreeItem : INotifyPropertyChanged
{
private bool _IsExpanded;
public bool IsExpanded
{
get { return _IsExpanded; }
set { _IsExpanded = value; OnPropertyChanged("IsExpanded"); }
}
private bool _IsSelected;
public bool IsSelected
{
get { return _IsSelected; }
set { _IsSelected = value; OnPropertyChanged("IsSelected"); }
}
public void Focus(TreeView tView)
{
TreeViewItem tvItem = (TreeViewItem)tView.ItemContainerGenerator.ContainerFromItem(this);
tvItem?.Focus();
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
And you can call it like this:
if (firstNode.HeaderText.Contains(textBox.Text))
{
firstNode.IsExpanded = true;
firstNode.IsSelected = true;
firstMatch.Focus();
}
ViewModel
public class MainWindowViewModel:BindableBase
{
public IRelayCommand MyCommand { get; protected set; }
private void CreateCommand()
{
this.MyCommand = new RelayCommand(MyCommandExecuted, CanExecuteMyCommand);
}
private void MyCommandExecuted(object obj)
{
MessageBox.Show("Command Executed");
}
private bool CanExecuteMyCommand(object obj)
{
return true; // The value is based on Selected Item
}
}
XAML
<ListBox
x:Name="myListBox"
ItemsSource="{Binding Path=MyClass}"
<ListBox.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Path=HeaderName}" IsExpanded="True">
<StackPanel>
<DataGrid
x:Name="dataGrid"
AutoGenerateColumns="False"
ItemsSource="{Binding Path=RowVal}" SelectedItem="{Binding CurrentItem}"/>
</StackPanel>
</Expander>
</DataTemplate>
</ListBox.ItemTemplate>
<Button Content="Select"
Command="{Binding Path=MyCommand }"
CommandParameter="{Binding ElementName=myListBox,Path=SelectedItem}"/>
DataClass
public class DataClass
{
public string HeaderName { get; set; }
public object RowVal { get; set; }
public ObservableCollection<DataGridColumn> ColumnCollection { get; set;}
private object currentItem;
public object CurrentItem
{
get
{
return currentItem;
}
set
{
currentItem = value;
}
}
}
How can I bind my button to Listbox item which is CurrentItem in DataClass ?
I created a complete example to show how I would do it. You would have to bind the parent element to SelectedItem as well, to keep track of when that item changes. Since the SelectedItem is public in your child class as well you can access that when your command triggers in your main view model.
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding Parents}" SelectedItem="{Binding SelectedParent}">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:Parent}">
<DataGrid ItemsSource="{Binding Children}" SelectedItem="{Binding SelectedChild}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Grid.Row="1" Width="70" Content="Click me" Height="25" Command="{Binding MyCommand}" />
i.e. in DoWork, you can get the child from the parent via its public property.
public sealed class WindowViewModel : INotifyPropertyChanged
{
private readonly ObservableCollection<Parent> parents;
private readonly ICommand myCommand;
private Parent selectedParent;
public WindowViewModel()
{
parents = new ObservableCollection<Parent>
{
new Parent{ Name = "P1"},
new Parent{ Name = "P2"}
};
myCommand = new DelegateCommand(DoWork);
}
private void DoWork()
{
var selectedChild = SelectedParent == null ? null : SelectedParent.SelectedChild;
}
public Parent SelectedParent
{
get { return selectedParent; }
set
{
if (selectedParent == value)
return;
selectedParent = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Parent> Parents
{
get { return parents; }
}
public ICommand MyCommand
{
get { return myCommand; }
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
With the basic setup of Data models
public class Parent : INotifyPropertyChanged
{
private ObservableCollection<Child> children;
private Child m_SelectedChild;
public Parent()
{
children = new ObservableCollection<Child>
{
new Child {Name = "C1"},
new Child {Name = "C2"}
};
}
public string Name { get; set; }
public ObservableCollection<Child> Children
{
get { return children; }
}
public Child SelectedChild
{
get { return m_SelectedChild; }
set
{
if (m_SelectedChild == value)
return;
m_SelectedChild = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Child
{
public string Name { get; set; }
}
Another solution, if you are more interested of the Child item in your WindowViewModel is to change the relative source of where the binding should occur, in your DataGrid. i.e., the binding would look like this instead:
<DataGrid ItemsSource="{Binding Children}"
SelectedItem="{Binding DataContext.SelectedChild, RelativeSource={RelativeSource AncestorType={x:Type ListView}}}" />
and then move the Property from Parent to WindowViewModel. With that you would be able to trigger changes to your button command when the child element changes for any of the Parent elements.
I think you want to pass the CurrentItem to the MyCommand as CommandParameter right?
Then you only have to:
CommandParameter="{Binding CurrentItem, UpdateSourceTrigger=PropertyChanged}"
Try this :
CommandParameter="{Binding ElementName=myListBox,Path=SelectedItem.CurrentItem}"