In my WPF MVVM application, when the (sub-)menu of a MenuItem opens, I want to populate the (sub-)menu.
E.g. there is a MenuItem "Logs". Only when the submenu opens, I want to search for corresponding log files on disk and display their filenames in the SubMenu afterwards.
Populating submenu dynamically
MainWindow.xaml
<Grid>
<Menu ItemsSource="{Binding MenuItems}">
<Menu.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:MyMenuItemViewModel}"
ItemsSource="{Binding Children}">
<local:CustomMenuItemControl/>
</HierarchicalDataTemplate>
</Menu.Resources>
</Menu>
</Grid>
MyMenuItemViewModel.cs
public class MyMenuItemViewModel : ObservableObject
{
public string Text { get; set; }
public ObservableCollection<MyMenuItemViewModel> Children { get; set; }
public MyMenuItemViewModel(string item)
{
Text = item;
Children = new ObservableCollection<MyMenuItemViewModel>();
}
}
My application is significantly larger, for illustrative purposes I have removed most of it.
I work with a ViewModel that contains a Text and an ObservableCollection "Children" for SubMenus.
It is displayed with a CustomControl that only displays the text.
However, I am already failing to get a trigger when the SubMenu is opened.
I've already tried adding event handler to HierarchicalDataTemplate and CustomMenuItemControl and
a DependencyProperty to the control and tried binding events in XAML, but apparently not in the right place.
Where exactly do I need to define the trigger or handler that executes code when the SubMenu is opened?
I'll answer my own question. Whether it's the best way to solve my problem, I don't know, but that's how I went about it:
In the ViewModel I created a property "IsSubMenuOpen". In the setter, I query whether the text matches "Logs". If yes, I fill the list with the log files.
private bool _isSubMenuOpen;
public bool IsSubMenuOpen
{
get => _isSubMenuOpen;
set
{
if(Text == "Logs")
{
Children.Clear();
foreach (var file in Directory.GetFiles(#"C:\MyDir", "*.log"))
{
Children.Add(new MyMenuItemViewModel(Path.GetFileName(file)));
}
}
SetProperty(ref _isSubMenuOpen, value);
}
}
In the MainWindow I have created a setter for the property "IsSubMenuopen", and bound my VM property in the value.
<Grid>
<Menu ItemsSource="{Binding MenuItems}">
<Menu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="IsSubmenuOpen" Value="{Binding IsSubMenuOpen}"></Setter>
</Style>
</Menu.ItemContainerStyle>
<Menu.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:MyMenuItemViewModel}"
ItemsSource="{Binding Children}">
<local:CustomMenuItemControl/>
</HierarchicalDataTemplate>
</Menu.Resources>
</Menu>
</Grid>
Related
This is a weird issue that I found in an MVVM project. I bound the IsSelected property of a ListBoxItem to an IsSelected property in the underlying model. If the collection holding the models bound to the list is too big, when you select a different user control and the focus is taken off of the ListBox; when you select an item in the list it will unselect every item EXCEPT the ones that are off-screen. The following gif shows this issue in a test project I made specifically for this issue;
MainView.xaml
<UserControl x:Class="ListBox_Selection_Issue.Views.MainView"
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:local="clr-namespace:ListBox_Selection_Issue.Views"
xmlns:vms="clr-namespace:ListBox_Selection_Issue.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.DataContext>
<vms:MainViewModel/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" SelectionMode="Extended" ItemsSource="{Binding FirstCollection}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Grid.Row="1"/>
</Grid>
</UserControl>
MainViewModel.cs
using System.Collections.ObjectModel;
namespace ListBox_Selection_Issue.ViewModels
{
class MainViewModel : ObservableObject
{
private ObservableCollection<CustomClass> _firstCollection;
public ObservableCollection<CustomClass> FirstCollection
{
get { return _firstCollection; }
set
{
_firstCollection = value;
OnPropertyChanged("FirstCollection");
}
}
public MainViewModel()
{
ObservableCollection<CustomClass> first = new ObservableCollection<CustomClass>();
for (int i = 1; i <= 300; i++)
{
first.Add(new CustomClass($"{i}"));
}
FirstCollection = first;
}
}
public class CustomClass
{
public string Name { get; set; }
public bool IsSelected { get; set; }
public CustomClass(string name)
{
Name = name;
IsSelected = false;
}
}
}
This is not how it works. If you understand UI virtualization, you should understand that virtualized containers (in your case ListBoxItem) are not part of the visual tree as they are removed as part of the virtualization process.
Because the WPF rendering engine has now far less containers to render, the performance is significantly improved. The effect becomes more relevant the more items the ItemsControl holds.
This is why you would never want to disable UI virtualization. This is why your posted solution can be qualified as a bad solution one should avoid.
ListBox is a Selector control. To allow it to work with any data, it must not be aware of the actual data models it renders. That's what the containers are for: anonymous wrappers that allow rendering and interaction logic without having the host to know the wrapped data object.
In your case, when ListBox.SelectionMode is set to SelectionMode.Extended or SelectionMode.Multiple, the ListBox will have to unselect all previously selected item in case the selection changes. Since it doesn't care about your data models, it only handles their associated wrappers: it will iterate over all ListBoxItem instances to change their state to e.g. unselected.
But the selection state will only be forwarded to the binding source for those Binding objects that are actually active (because the binding target is currently realized/visible).
Although all containers, virtualized and realized, will be unselected, the Binding of those virtualized containers won't update (because the corresponding container is not active and removed - removing includes clearing their ListBoxItem.Content property too).
As a matter of fact, unless container recycling is explicitly enabled by setting VirtualizingPanel.VirtualizationMode to VirtualizationMode.Recycling, virtualized container instances are removed by simply making them eligible for garbage collection. They are just dropped and gone without any further modification e.g., of the ListBoxItem.IsSelected property and new container instances are created for newly realized items.
Now when you scroll the virtualized items into the view, the ListBox will generate the containers and will set the ListBoxItem.Content property to the wrapped data item. Since in your case the data model, the CustomClass, still holds the previous and now outdated selection state (which is still "selected"), the realized containers will change their state back to selected (via the reactivated data binding).
That's why in your case virtualized items remain selected. And this is because you bind the container's state to your data model in a multiselect scenario with UI virtualization engaged.
ListBox selection states are meant to be handled via the Selector.SelectedItem and ListBox.SelectedItems properties or the Selector.SelectionChanged event. This is not important in single select mode but essential in multiselect mode.
Since ListBox.SelectedItems is a read-only property, you can't set a binding on it (like you would usually do with the SelectedItem property).
There are many ways you can get the SeletedItems value to your DataContext. The most straight forward would be to send it from the Selector.SelectionChanged event.
From a design perspective, you should generally never set the DataContext of a UserControl, or Control in general, explicitly. The DataContext should be inherited from the parent element that hosts your custom control.
A View-Model-per-View approach is always to avoid. It will make your control very specific to a particular data type. And vice versa, it will make your View Model too specific for a particular element of the View. Additionally, it will introduce other design problems that may even lead to break MVVM.
The DataContext must always be set outside the control (from the client context), so that the control doesn't know the concrete DataContext class.
The improved solution could look as follows:
MainWindow.xaml
<Window>
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<local:MainView ItemsSource="{Binding FirstCollection}"
SelectedItems="{Binding SelectedCustomClassModels}">
<local:MainView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</local:MainView.ItemTemplate>
</local:MainView>
</Grid>
</Window>
MainView.xaml.cs
public partial class MainView : UserControl
{
public IList SelectedItems
{
get => (IList)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(IList),
typeof(MainView),
new FrameworkPropertyMetadata(default(IList), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public IList ItemsSource
{
get => (IList)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource",
typeof(IList),
typeof(MainView), new PropertyMetadata(default));
public DataTemplate ItemTemplate
{
get => (DataTemplate)GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register(
"ItemTemplate",
typeof(DataTemplate),
typeof(MainView), new PropertyMetadata(default));
public MainView()
{
InitializeComponent();
}
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// You can track particular items...
IList newSelectedItems = e.AddedItems;
IList newUnselectedItems = e.RemovedItems;
// ... or the final result
var listBox = sender as ListBox;
this.SelectedItems = listBox.SelectedItems;
}
}
MainView.xaml
<UserControl>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
SelectionMode="Extended"
SelectionChanged="ListBox_SelectionChanged"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ItemsSource}"
ItemTemplate="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ItemTemplate}" />
<Button Grid.Row="1" />
</Grid>
</UserControl>
MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<CustomClass> FirstCollection { get; private set; }
private IList selectedCustomClassModels;
public IList SelectedCustomClassModels
{
get => this.selectedCustomClassModels;
set
{
this.selectedCustomClassModels = value;
OnPropertyChanged();
OnSelectedCustomClassModelsChanged();
}
}
public MainViewModel()
{
this.FirstCollection = new ObservableCollection<CustomClass>();
for (int i = 1; i <= 300; i++)
{
this.FirstCollection.Add(new CustomClass($"{i}"));
}
this.SelectedCustomClassModels = new List<object>();
}
private void OnSelectedCustomClassModelsChanged()
{
// TODO::Handle selected items
IEnumerable<CustomClass> selectedItems = this.SelectedCustomClassModels
.Cast<CustomClass>();
}
}
Finding the fix to this issue took me longer than I would have expected, so I figured I would share my knowledge.
Add VirtualizingStackPanel.IsVirtualizing="False" to the ListBox like in the following file:
<UserControl x:Class="ListBox_Selection_Issue.Views.MainView"
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:local="clr-namespace:ListBox_Selection_Issue.Views"
xmlns:vms="clr-namespace:ListBox_Selection_Issue.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.DataContext>
<vms:MainViewModel/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" SelectionMode="Extended" VirtualizingStackPanel.IsVirtualizing="False" ItemsSource="{Binding FirstCollection}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Grid.Row="1"/>
</Grid>
</UserControl>
I found out that the issue had to do with "Virtualization" from this answer (Select Show more comments). Then I started looking into Listbox Virtualization. I lost the tab where I got the answer from. So, I can't credit them. Hopefully, this will help someone in the future.
I am trying to implement in WPF a way for the user to select multiple items in one box and via button click add those selected items to the other box.
I am trying to adhere to MVVM w/ minimal code behind. The solutions I find show the DataContext being manipulated via the View code behind which I am trying to avoid.
I think my issue is I do not know how to toggle the IsSelected from xaml, but not sure.
XAML
<ListView
ItemsSource="{Binding AvailableStates, Mode=TwoWay}"
SelectedItem="{Binding SelectedStates, Mode=TwoWay}"
SelectionMode="Multiple"
DisplayMemberPath="state"
Grid.Row="1"
Margin="5"
Grid.Column="1"
Height="125"
Name="lvAvailableStates"
Grid.RowSpan="6"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.CanContentScroll="True">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
<Button
Grid.Row="2"
Grid.Column="2"
Margin="10"
Command="{Binding AddSelectedStatesCommand}"
Content=">>" />
ViewModel
private ObservableCollection<SelectableItemWrapper<states_approved>> _selectedStates;
public ObservableCollection<SelectableItemWrapper<states_approved>> SelectedStates
{
get { return _selectedStates; }
set
{
_selectedStates = value;
OnPropertyChanged();
}
}
private void AddSelectedStates(object obj)
{
var selected = SelectedStates.Where(s => s.IsSelected)
.Select(s => s.Item)
.ToList();
StatesApproved = selected;
}
public CustomCommand AddSelectedStatesCommand
{
get
{
return new CustomCommand(AddSelectedStates, CanExecute);
}
}
Selected Item Wrapper
public class SelectableItemWrapper<T>
{
public bool IsSelected { get; set; }
public T Item { get; set; }
}
ListView has internal property to determine which item is selected and it also has SelectedItems to determine multiple selected items. However, this plural SelectedItems of ListView is not bindable. So, the solution is to pass them as a CommandParameter.
<ListView x:Name="lvAvailableStates"
ItemsSource="{Binding AvailableStates, Mode=TwoWay}"
SelectedItem="{Binding SelectedStates, Mode=TwoWay}" => remove this!
...
<Button Command="{Binding AddSelectedStatesCommand}"
CommandParameter="{Binding SelectedItems, Mode=OneWay, ElementName=lvAvailableStates}" => add this!
...
In the VM
private void AddSelectedStates(IEnumerable<SelectableItemWrapper<states_approved>> selectedItems)
{
StatesApproved = selectedItems
.Select(s => s.Item) // only retrieve the Item
.ToList();
}
As you can see at this point, you don't even really need the SelectableItemWrapper to set/unset the IsSelected property to begin with. You should just remove the wrapper and life will be easier.
Currently I am trying to display a ObservableCollection of an custom class in a TreeView, when the user double clicks on a 'item' it will fire an method in the ViewModel passing the selected custom class as parameter. I am using the MVVM structure for my WPF Application.
The problem I am facing with this is that the Observable Collection is displayed with an HierarchicalDataTemplate. See underneath the whole XAML code for the TreeView
<TreeView Name="DeviceTreeView" ItemsSource="{Binding ViewableTIADeviceTree}" Grid.Column="3" Margin="5">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type treeviewable:ViewableTIADevice}" ItemsSource="{Binding DeviceItems}">
<TextBlock Text="{Binding Path=DeviceName}"/>
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="{x:Type treeviewable:ViewableTIADevice}">
<Setter Property="commandBehaviors:MouseDoubleClick.Command"
Value="{Binding TIADeviceTreeItemDoubleClick}"/>
<Setter Property="commandBehaviors:MouseDoubleClick.CommandParameter"
Value="{Binding}"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type treeviewable:ViewableDeviceItem}">
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
</TreeView.Resources>
</TreeView>
And the MouseDoubleClick attached behavior class:
public class MouseDoubleClick
{
public static DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command",
typeof(ICommand),
typeof(MouseDoubleClick),
new UIPropertyMetadata(CommandChanged));
public static DependencyProperty CommandParameterProperty =
DependencyProperty.RegisterAttached("CommandParameter",
typeof(object),
typeof(MouseDoubleClick),
new UIPropertyMetadata(null));
public static void SetCommand(DependencyObject target, ICommand value)
{
target.SetValue(CommandProperty, value);
}
public static void SetCommandParameter(DependencyObject target, object value)
{
target.SetValue(CommandParameterProperty, value);
}
public static object GetCommandParameter(DependencyObject target)
{
return target.GetValue(CommandParameterProperty);
}
public static void CommandChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
Control control = target as Control;
if(control != null)
{
if((args.NewValue != null) && (args.OldValue == null))
{
control.MouseDoubleClick += OnMouseDoubleClick;
}
else if((args.NewValue == null) && (args.OldValue != null))
{
control.MouseDoubleClick -= OnMouseDoubleClick;
}
}
}
private static void OnMouseDoubleClick(object sender, RoutedEventArgs e)
{
Control control = sender as Control;
ICommand command = (ICommand)control.GetValue(CommandProperty);
object commandParameter = control.GetValue(CommandParameterProperty);
command.Execute(commandParameter);
}
}
The problem I am facing with this is that it says that the 'ViewableTIADevice' is not an FrameWorkElement and thus I cannot even run it.
I've also tried using the
<Style TargetType"{x:Type TreeViewItem}">
That does run but I get no response when trying to double click an item in the TreeView.
I've searched a lot for the solution and I would like to refer to this thread: WPF/MVVM - how to handle double-click on TreeViewItems in the ViewModel?
I've been using the above thread as solution but how can I combine that solution with an HierarchicalDatatemplate?
EDIT
The ICommand that I am trying to call by double clicking an item
public RelayCommand TIADeviceTreeItemDoubleClick { get; set; }
Where I am here assigning it to the function
TIADeviceTreeItemDoubleClick = new RelayCommand(c => tiaDeviceTreeItemDoubleClick(c));
And the function it refers to:
private void tiaDeviceTreeItemDoubleClick(object value)
{
//code
}
This is the ViewableTIADevice class:
public class ViewableTIADevice
{
public ViewableTIADevice()
{
DeviceItems = new List<ViewableDeviceItem>();
}
public string DeviceName { get; set; }
public IList<ViewableDeviceItem> DeviceItems { get; set; }
}
i believe you have a missunderstanding of what your datacontext is, consider this example:
<TreeView ItemsSource="{Binding Items}">
<TreeView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ItemProperty1}"></TextBlock>
</DataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="Local:MouseDoubleClick.Command"
Value="{Binding ElementName=DeviceTreeView, Path=DataContext.TIADeviceTreeItemDoubleClick}"/>
<Setter Property="Local:MouseDoubleClick.CommandParameter"
Value="{Binding}"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
the datacontext of my TreeView, is my viewmodel, so when i say ItemsSource="{Binding Items}", i am binding to the observable collection called Items inside my ViewModel.
the datacontext inside TreeView.ItemTemplate, is one single item inside the collection Items. meaning that when i say {Binding ItemProperty1}, i am binding, NOT to ViewModel.ItemProprty1, but to a single item inside the collection ViewModel.Items. this means that you have to have a ViewModel, and inside a collection called Items, and inside this collection you need to have objects of type X, and the class X must have a property called ItemProperty1.
the datacontext inside the TreeView.ItemContainerStyle, is also one single item inside the collection Items, meaning that when you say {Binding TIADeviceTreeItemDoubleClick}, you are trying to bind to an ICommand property that is inside of the class that is inside your collection Items. your datacontext here, is not, as you assumed, your ViewModel, but rather one single item inside ViewModel.Items
so when you use this:
Value="{Binding ElementName=DeviceTreeView, Path=DataContext.TIADeviceTreeItemDoubleClick}"
you are binding to the datacontext of the TreeView, which is ViewModel, which contains an ICommand property called TIADeviceTreeItemDoubleClick.
when you write this:
{Binding TIADeviceTreeItemDoubleClick}
you are trying to bind to the datacontext of the current TreeViewItem, which is one single object inside your collection Items. so this will only work if you add the ICommand to your class that is inside your collection Items.
you also use this:
<Setter Property="Local:MouseDoubleClick.CommandParameter"
Value="{Binding}"
here, obviosly, you are sending one single item inside your collection Items, and not the ViewModel.
make sense?
I have the following XAML used to populate a sub-MenuItem listing with RecentDocuments:
<MenuItem Header="_Recent Studies"
ItemsSource="{Binding RecentFiles}"
AlternationCount="{Binding Path=Items.Count,
Mode=OneWay,
RelativeSource={RelativeSource Self}}"
ItemContainerStyle="{StaticResource RecentMenuItem}"/>
Where in the ViewModel I have the following RecentFiles property
private ObservableCollection<RecentFile> recentFiles = new ObservableCollection<RecentFile>();
public ObservableCollection<RecentFile> RecentFiles
{
get { return this.recentFiles; }
set
{
if (this.recentFiles == value)
return;
this.recentFiles = value;
OnPropertyChanged("RecentFiles");
}
}
Now this works fine and displays my recent menu items like so:
My question is; how can I bind to the click event on my recent files MenuItems? I am amble to use AttachedCommands but I don't see how this can be achieved.
thanks for your time.
If you are using MVVM pattern, you do not need Click event at all.
You should use MenuItem.Command property in order to communicate with your ViewModel.
HOW?
As I can see, you are using the ItemContainerStyle. You can add the following line to that style:
<Style x:Key="RecentMenuItem" TargetType="MenuItem">
...
<Setter Property="Command" Value="{Binding Path=SelectCommand}" />
...
</Style>
And in your RecentFile:
public ICommand SelectCommand { get; private set; }
You can initialize the command inside the constructor of RecentFile class.
I have an application with a WPF treeview with a node hierarchy. I have to display a context menu for the one or more selected nodes. When one or more nodes are selected, a collection in my viewmodel gets populated with all those selected nodes.
I have a collection of menuitems binded to my treeview contextmenu. I only want this binding to be evaluated when user right clicks on the node(or nodes).
To be bit more specific here is what I want:
User clicks on one or more menu items to select them
He right clicks for bringing up the contextmenu, I need my contextmenu biding(MenuItems) to be evaluated at this point in time and not while the user clicks on each menu itmes as is happening now.
Below is my code:
<TreeView MinWidth="100" ItemsSource="{Binding Nodes}">
<i:Interaction.Behaviors>
<Behaviors1:BindableSelectedItemBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
</i:Interaction.Behaviors>
<TreeView.ContextMenu>
<ContextMenu ItemsSource="{Binding MenuItems}" Visibility="{Binding ShowContextMenu, Converter={StaticResource VisibilityConverter}}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Name}"/>
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="Command" Value="{Binding MenuCommand}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
And my ViewModel:
internal class MyViewModel : NotificationObject
{
private readonly IContextMenuProvider _contextMenuProvider;
public MyViewModel(IContextMenuProvider contextMenuProvider)
{
_contextMenuProvider = contextMenuProvider;
}
public ObservableCollection<IMenuItem> MenuItems
{
get
{
System.Diagnostics.Debug.WriteLine("Getting menu items");
return GetMenuItems();
}
}
private ObservableCollection<INodeViewModel> _selectedNodes;
public ObservableCollection<INodeViewModel> SelectedNodes
{
get { return _selectedNodes; }
set
{
_selectedNodes = value;
System.Diagnostics.Debug.WriteLine("Setting selected nodes");
foreach (var nodeViewModel in _selectedNodes)
{
System.Diagnostics.Debug.WriteLine(nodeViewModel.Name);
}
RaisePropertyChanged(() => SelectedNodes);
RaisePropertyChanged(() => ShowContextMenu);
RaisePropertyChanged(() => MenuItems);
}
}
public bool ShowContextMenu
{
get
{
var canDisplay = _contextMenuProvider.GetMenuItemsByNodeContext(SelectedNodes);
return !canDisplay.IsNullOrEmpty();
}
}
private ObservableCollection<IMenuItem> GetMenuItems()
{
var items = _contextMenuProvider.GetAllMenuItems(SelectedNodes);
var menuItems = new ObservableCollection<IMenuItem>(items);
return menuItems;
}
}
The issues I'm facing is: I dont know at which point should I fetch the menu items, should I do it while selectednodes collection is getting populated or on right click by user? I want either one of them happening ideally during the right click, question how do I refresh my treeview contextmenu bindings while right clicking a node on the treeview?
Note: I have a selected property on the NodeViewModel for selection purposes.
Thanks,
-Mike