Dynamic Context Menu on WPF TreeView - c#

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

Related

MenuItem - CustomControl with Event Handler for IsSubMenuOpen

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>

WPF ListView Multi-Select MVVM w/ minimal code behind

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.

Menu item that sets properties on menu open

I currently trying to create a dynamically created context menu. I'm currently binding a ObservableCollection<MenuItem> to the ItemsSource property on the context menu. I now want to set the visibility of the items in the list when the menu opens depending on what I have selected.
I tried Inheriting from MenuItem like this
public class CtContextMenuItem : MenuItem
{
public delegate Visibility VisibilityDelegate();
public VisibilityDelegate IsVisibleDelegate = null;
}
And I want to set Visibility to the result of the VisibilityDelegate when the context menu is opened but I can't find any event or method that is called on the MenuItem when the context menu is opened
Is there a way to do it or is it do I have to just create all the items of the menu inside a function listening to ContextMenuOpening?
Bind the ItemsSource to an ObservableCollection<CtContextMenuItem> where the CtContextMenuItem type has a Visibility or bool property that you can bind to in your XAML. Something like this:
public class CtContextMenuItem
{
public Visibility IsVisible { get; set; }
}
<ContextMenu ItemsSource="{Binding TheSourceCollection}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Visibility" Value="{Binding IsVisible}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>

List Box items with checkboxes, multiselect not working properly in WPF MVVM

So I have a ListBox with CheckBox-es that have the IsChecked property bound to the Item's property called IsSelected. That produces a weird behavior where if I click on the item itself it checks the checkbox (good) and sets the property on the item (good), but doesn't actually select the item in the list box, ie. the highlighting isn't there. I am guessing that the ListBox IsSelected property needs to be set as well for that right?
Now, I am trying to get the multi-select behavior to work so I changed the SelectionMode to Extended. Now, I can select only Items, not the checkboxes. What happens is that if I use SHIFT + click by pointing at the area next to the item, not the item itself, then it select multiple items, but clicking on the items themselves doesn't do the trick of multi-selection not does it check the checkboxes. What is going on in here?
I would like to be able to select multiple items by holding shift etc, and have that trigger the property on the Elevation item so I know which ones are checked. Any help is appreciated.
Here's my XAML:
<ListBox x:Name="LevelsListBox"
ItemsSource="{Binding Elevations, UpdateSourceTrigger=PropertyChanged}"
SelectionMode="Extended"
BorderThickness="0">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected}" Content="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
My View Model:
public class AxoFromElevationViewModel : ViewModelBase
{
public AxoFromElevationModel Model { get; }
public RelayCommand CheckAll { get; }
public RelayCommand CheckNone { get; }
public AxoFromElevationViewModel(AxoFromElevationModel model)
{
Model = model;
Elevations = Model.CollectElevations();
CheckAll = new RelayCommand(OnCheckAll);
CheckNone = new RelayCommand(OnCheckNone);
}
private void OnCheckNone()
{
foreach (var e in Elevations)
{
e.IsSelected = false;
}
}
private void OnCheckAll()
{
foreach (var e in Elevations)
{
e.IsSelected = true;
}
}
/// <summary>
/// All Elevation Wrappers.
/// </summary>
private ObservableCollection<ElevationWrapper> _elevations = new ObservableCollection<ElevationWrapper>();
public ObservableCollection<ElevationWrapper> Elevations
{
get { return _elevations; }
set { _elevations = value; RaisePropertyChanged(() => Elevations); }
}
}
Finally my Elevation Class:
public sealed class ElevationWrapper : INotifyPropertyChanged
{
public string Name { get; set; }
public ElementId Id { get; set; }
public object Self { get; set; }
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set { _isSelected = value; RaisePropertyChanged("IsSelected"); }
}
public ElevationWrapper(View v)
{
Name = v.Name;
Id = v.Id;
Self = v;
IsSelected = false;
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
You should bind the IsSelected property of your ListBoxItems to the IsSelected property of your view model. This way CheckBoxes will trigger the selection and when you select an item, the related CheckBox will be checked.
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
It seems to me you want to sync 3 properties ListBoxItem.IsSelected, CheckBox.IsChecked and your models IsSelected.
My advice is that only one of the templates/styles should bind to the underlying model so I will add Yusuf answer as I will use the ListBoxItem style to bind to your model property.
After that you should bind the Checkbox.IsChecked to the ListBoxItem.IsSelected and your ListBox should look like this:
<ListBox x:Name="LevelsListBox"
ItemsSource="{Binding Elevations, UpdateSourceTrigger=PropertyChanged}"
SelectionMode="Extended"
BorderThickness="0">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Content="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Always try to bind XAML properties in a chain way, e.g. model.A binds to Model.B binds to Model.C, doing this should help you keep updates consistent and avoid wierd cases.
There is an issue with this code though, after you select multiple items and click one check box it will only unselect that item but if you click another item it will unselect all except that item.

Sub-MenuItem Selection in MVVM

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.

Categories