C
C#9mo ago
PepeGak

✅ How to handle commands in TreeView?

According to the methodology by which I do this practical work, I need to visualize the territorial division of Kazakhstan using TreeView, using C# + WPF and the MVVM. What is the best way for me to make sure that I have 2 hierarchies (oblasts and cities), and that when I click on an object from the list, a command is triggered and displays information about this object to me (population, area, image, etc.)
14 Replies
PepeGak
PepeGak9mo ago
I tried this
<TreeView Grid.Column="0">
<TreeViewItem Header="Cities" ItemsSource="{Binding Cities}">
<TreeViewItem.ItemTemplate>
<HierarchicalDataTemplate>
<Button Content="{Binding SubjectName}"
Command="{Binding ShowSubject}"/>
</HierarchicalDataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
<!--Same for oblasts, I guess-->
</TreeView>
<TreeView Grid.Column="0">
<TreeViewItem Header="Cities" ItemsSource="{Binding Cities}">
<TreeViewItem.ItemTemplate>
<HierarchicalDataTemplate>
<Button Content="{Binding SubjectName}"
Command="{Binding ShowSubject}"/>
</HierarchicalDataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
<!--Same for oblasts, I guess-->
</TreeView>
but it doesn't seem to work Cities here is ObservableCollection<Subject> What is the propriate way to write a command for that? So when I click on a button (or any other thing that supports command implementation), object from that collection is taken and shown on the window (somewhere on Grid.Column="1")
lycian
lycian9mo ago
Is ShowSubject an ICommand? If so, it should take something like an index or the object itself with CommandParameter
Mayor McCheese
Mayor McCheese9mo ago
<Window x:Class="CommandSample.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:CommandSample"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" d:DataContext="{d:DesignInstance Type=local:MainWindowViewModel}">
<DockPanel>
<ItemsControl DockPanel.Dock="Top" ItemsSource="{Binding Path=Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Path=Name}" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.MyCommand}" CommandParameter="{Binding}"></Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

<TextBox Text="{Binding Path=Item.Name}"></TextBox>
</DockPanel>
</Window>
<Window x:Class="CommandSample.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:CommandSample"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" d:DataContext="{d:DesignInstance Type=local:MainWindowViewModel}">
<DockPanel>
<ItemsControl DockPanel.Dock="Top" ItemsSource="{Binding Path=Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Path=Name}" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.MyCommand}" CommandParameter="{Binding}"></Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

<TextBox Text="{Binding Path=Item.Name}"></TextBox>
</DockPanel>
</Window>
public class MainWindowViewModel : INotifyPropertyChanged
{
private Item _item = new Item();

public ObservableCollection<Item> Items { get; set; }

public ICommand MyCommand { get; set; }

public MainWindowViewModel()
{
Items = new ObservableCollection<Item>(new Item[] { new Item { Name = "Test" } });
MyCommand = new Command(this);
}

public Item Item
{
get => _item;
set
{
_item = value;
OnPropertyChanged();
}
}

internal class Command : ICommand
{
private readonly MainWindowViewModel _mainWindowViewModel;

public Command(MainWindowViewModel mainWindowViewModel)
{
_mainWindowViewModel = mainWindowViewModel;
}

public bool CanExecute(object? parameter)
{
return true;
}

public void Execute(object? parameter)
{
if (parameter is Item item)
{
_mainWindowViewModel.Item = item;
}
}

public event EventHandler? CanExecuteChanged;
}

public event PropertyChangedEventHandler? PropertyChanged;

protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}

public class Item
{
public string Name { get; set; }
}
public class MainWindowViewModel : INotifyPropertyChanged
{
private Item _item = new Item();

public ObservableCollection<Item> Items { get; set; }

public ICommand MyCommand { get; set; }

public MainWindowViewModel()
{
Items = new ObservableCollection<Item>(new Item[] { new Item { Name = "Test" } });
MyCommand = new Command(this);
}

public Item Item
{
get => _item;
set
{
_item = value;
OnPropertyChanged();
}
}

internal class Command : ICommand
{
private readonly MainWindowViewModel _mainWindowViewModel;

public Command(MainWindowViewModel mainWindowViewModel)
{
_mainWindowViewModel = mainWindowViewModel;
}

public bool CanExecute(object? parameter)
{
return true;
}

public void Execute(object? parameter)
{
if (parameter is Item item)
{
_mainWindowViewModel.Item = item;
}
}

public event EventHandler? CanExecuteChanged;
}

public event PropertyChangedEventHandler? PropertyChanged;

protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}

public class Item
{
public string Name { get; set; }
}
Notice the part CommandParameter="{Binding}"; personally I feel like this is non-intuitive and not very discoverable.
Mayor McCheese
Mayor McCheese9mo ago
No description
No description
Mayor McCheese
Mayor McCheese9mo ago
@PepeGak see my answer above; if it works consider closing with /close.
PepeGak
PepeGak9mo ago
Hi. I've tried the next thing: I created an ObservableCollection (let's call it OC) of TreeItemViewModel (TIVM) and inside the TIVM class there is OC<SubjectModel> of actual Subjects and string SubjectType {get;set;}
PepeGak
PepeGak9mo ago
So now it looks like this (it's all in Russian but it works)
No description
PepeGak
PepeGak9mo ago
and redid commands thing. I found RelayCommand implementation and it works fine for me
public class RelayCommand : ICommand
{
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}

private Action<object> execute;
private Func<object, bool> canExecute;

public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute;
}

public bool CanExecute(object parameter)
=> this.canExecute == null || this.canExecute(parameter);
public void Execute(object parameter)
=> this.execute(parameter);
}
public class RelayCommand : ICommand
{
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}

private Action<object> execute;
private Func<object, bool> canExecute;

public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute;
}

public bool CanExecute(object parameter)
=> this.canExecute == null || this.canExecute(parameter);
public void Execute(object parameter)
=> this.execute(parameter);
}
But there is now another problem. Let's say I choose a subject (Astana city for example). How do I access it using TreeView? Just iterate through all OC's until the subject itself is found?
Mayor McCheese
Mayor McCheese9mo ago
You don't really access treeview directly in my example, I have the command at the root level which contains the command to handle the treeviewitem command and your treeviewitem command is bound to the SubjectModel
PepeGak
PepeGak9mo ago
Best thing I could do is:
private void OnTreeItemSelected(object sender, RoutedEventArgs e)
{
if (sender is TreeView trVw)
{
this.MWVM.Tree_View = trVw;

if (trVw.SelectedItem is SubjectModel SM)
this.MWVM.SelectedSubject = SM;
else if (trVw.SelectedItem is TreeItemViewModel)
this.MWVM.SelectedSubject = null;
}
}
private void OnTreeItemSelected(object sender, RoutedEventArgs e)
{
if (sender is TreeView trVw)
{
this.MWVM.Tree_View = trVw;

if (trVw.SelectedItem is SubjectModel SM)
this.MWVM.SelectedSubject = SM;
else if (trVw.SelectedItem is TreeItemViewModel)
this.MWVM.SelectedSubject = null;
}
}
and XAML code for TreeView tag is
<TreeView Grid.Column="0" Grid.Row="1" x:Name="tree1" ItemsSource="{Binding GRZ_and_OBL}"
TreeViewItem.Selected="OnTreeItemSelected">
<TreeView Grid.Column="0" Grid.Row="1" x:Name="tree1" ItemsSource="{Binding GRZ_and_OBL}"
TreeViewItem.Selected="OnTreeItemSelected">
So when the item is selected, treeview is copied in my MainWindowViewModel and in my viewmodel I'm working with Tree_View I'm pretty sure that it violates the MVVM pattern but I hope my teachers accept that
Mayor McCheese
Mayor McCheese9mo ago
in my command I have
internal class Command : ICommand
{
private readonly MainWindowViewModel _mainWindowViewModel;

public Command(MainWindowViewModel mainWindowViewModel)
{
_mainWindowViewModel = mainWindowViewModel;
}

public bool CanExecute(object? parameter)
{
return true;
}

public void Execute(object? parameter)
{
if (parameter is Item item)
{
_mainWindowViewModel.Item = item;
}
}

public event EventHandler? CanExecuteChanged;
}
internal class Command : ICommand
{
private readonly MainWindowViewModel _mainWindowViewModel;

public Command(MainWindowViewModel mainWindowViewModel)
{
_mainWindowViewModel = mainWindowViewModel;
}

public bool CanExecute(object? parameter)
{
return true;
}

public void Execute(object? parameter)
{
if (parameter is Item item)
{
_mainWindowViewModel.Item = item;
}
}

public event EventHandler? CanExecuteChanged;
}
Which sets the reference when the command is executed
PepeGak
PepeGak9mo ago
I don't quite understand what does this command do
Mayor McCheese
Mayor McCheese9mo ago
in your xaml, you have
<TreeView Grid.Column="0">
<TreeViewItem Header="Cities" ItemsSource="{Binding Cities}">
<TreeViewItem.ItemTemplate>
<HierarchicalDataTemplate>
<Button Content="{Binding SubjectName}"
Command="{Binding ShowSubject}"/>
</HierarchicalDataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
<!--Same for oblasts, I guess-->
</TreeView>
<TreeView Grid.Column="0">
<TreeViewItem Header="Cities" ItemsSource="{Binding Cities}">
<TreeViewItem.ItemTemplate>
<HierarchicalDataTemplate>
<Button Content="{Binding SubjectName}"
Command="{Binding ShowSubject}"/>
</HierarchicalDataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
<!--Same for oblasts, I guess-->
</TreeView>
When you click the treeview, a command will be sent What you are attempting to do, is to take the subject of that command ( the command parameter ) and push it onto the reference for your main view model how the command accomplishes that is covered in three areas;. first...
public MainWindowViewModel()
{
Items = new ObservableCollection<Item>(new Item[] { new Item { Name = "Test" } });
MyCommand = new Command(this);
}
public MainWindowViewModel()
{
Items = new ObservableCollection<Item>(new Item[] { new Item { Name = "Test" } });
MyCommand = new Command(this);
}
Here the view model has the command, and a reference passed into the command for the viewmodel MyCommand = new Command(this); secondly we have
public void Execute(object? parameter)
{
if (parameter is Item item)
{
_mainWindowViewModel.Item = item;
}
}
public void Execute(object? parameter)
{
if (parameter is Item item)
{
_mainWindowViewModel.Item = item;
}
}
This is processed when the command is executed; the parameter is Item item is a pattern match type check, if it's an Item ( or in your case a subject ) it creates a reference to the type as item. THen next, sets the mainviewmodel.Item to the typed item. In your case, consider Item as Subject. lastly in the xaml I have...
<Button Content="{Binding Path=Name}" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.MyCommand}" CommandParameter="{Binding}"></Button>
<Button Content="{Binding Path=Name}" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.MyCommand}" CommandParameter="{Binding}"></Button>
Here the command is bound to the root of the DataContext via the FindAncestor method; and the parameter, {Binding} would send your subject to the command. so the reasons for Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.MyCommand}" is that we only need one command to handle any Subject; but because you're in a HierachalDataTemplate, we have to find the Window.DataContext to set the proper command @PepeGak so in practice, your button is the same as mine, it's executing a bound command, I'm just passing in the subject to the parameter. Code like this can be confusing to follow; it eliminates having multiple types of commands, but at the expense of mixing a lot of code together. You could have something like...
class MainWindowViewModel : INotifyPropertyChanged
{
public SubjecModel SelectedSubjectModel
{
get=>_selectedSubjectModel;
set {
_selectedSubjectModel = value;
NotifyPropertChanged();
}
}

public ObservableColl3etion<SubjectModel> SubjectModelCollection {get;}

public ICommand SubjectChangedCommand {get;}

public MainWindowViewModel()
{
SubjectModelCollection = InitializeSubjectModelCollectionSomeHow();

SubjectChangedCommand = new RelayCommand(
s=> {
if(s is SubjectModel subjectModel)
{
_selectedSubjectModel = subjectModel;
}
},
s=> s is SubjectModel;
);
}
}
class MainWindowViewModel : INotifyPropertyChanged
{
public SubjecModel SelectedSubjectModel
{
get=>_selectedSubjectModel;
set {
_selectedSubjectModel = value;
NotifyPropertChanged();
}
}

public ObservableColl3etion<SubjectModel> SubjectModelCollection {get;}

public ICommand SubjectChangedCommand {get;}

public MainWindowViewModel()
{
SubjectModelCollection = InitializeSubjectModelCollectionSomeHow();

SubjectChangedCommand = new RelayCommand(
s=> {
if(s is SubjectModel subjectModel)
{
_selectedSubjectModel = subjectModel;
}
},
s=> s is SubjectModel;
);
}
}
Then you have your hierarchal data template for your treeview.
<TreeView Grid.Column="0">
<TreeViewItem Header="Cities" ItemsSource="{Binding SubjectModelCollection}">
<TreeViewItem.ItemTemplate>
<HierarchicalDataTemplate>
<Button Content="{Binding SubjectName}"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.SubjectChangedCommand}"
CommandParameter="{Binding}"></Button>
</HierarchicalDataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
</TreeView>
<TreeView Grid.Column="0">
<TreeViewItem Header="Cities" ItemsSource="{Binding SubjectModelCollection}">
<TreeViewItem.ItemTemplate>
<HierarchicalDataTemplate>
<Button Content="{Binding SubjectName}"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.SubjectChangedCommand}"
CommandParameter="{Binding}"></Button>
</HierarchicalDataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
</TreeView>
btw... this part here is what I mean by confusing and difficult to follow...
SubjectChangedCommand = new RelayCommand(
s=> {
if(s is SubjectModel subjectModel)
{
_selectedSubjectModel = subjectModel;
}
},
s=> s is SubjectModel;
);
SubjectChangedCommand = new RelayCommand(
s=> {
if(s is SubjectModel subjectModel)
{
_selectedSubjectModel = subjectModel;
}
},
s=> s is SubjectModel;
);
In an effort to avoid a dedicated class we have a single type of command. It's not wrong, I just personally don't like it; it can be hard for someone to follow, especially in larger implementations.
PepeGak
PepeGak9mo ago
Hmm it seems to fit my problem i'll try it. Thanks!
Want results from more Discord servers?
Add your server