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>
Related
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>
HOW do I pass the value of the TextBox to the StyleSelector so I can return the correct Style?
I want to be able to choose the background of a row conditionally by comparing a property of the object shown in the row with the value of an input control (e.g TextBox).
I made a StyleSelector class containing two styles and a property to contain the value of the TextBox.
public class OutOfBoundsRowStyleSelector : StyleSelector
{
public double FilterValue { get; set; }
public Style DefaultStyle { get; set; }
public Style OutOfBoundsStyle { get; set; }
public override Style SelectStyle(object item, DependencyObject container)
{
// Compare FilterValue to property of item and return the appropriate style here.
return DefaultStyle;
}
}
I added a TextBox and bound the Value to a property in the ViewModel like so.
<TextBox x:Name="txtBoundsFilter" Text="{Binding BoundsFilter}"/>
I filled these styles in xaml & tried to bind the TextBox value to the property in the StyleSelector class.
<Classes:OutOfBoundsRowStyleSelector x:Key="OutOfBoundsRowStyleSelector">
<Classes:OutOfBoundsRowStyleSelector.DefaultStyle>
<Style TargetType="telerik:GridViewRow">
<Setter Property="Background" Value="White"/>
</Style>
</Classes:OutOfBoundsRowStyleSelector.DefaultStyle>
<Classes:OutOfBoundsRowStyleSelector.OutOfBoundsStyle>
<Style TargetType="telerik:GridViewRow">
<Setter Property="Background" Value="Red" />
</Style>
</Classes:OutOfBoundsRowStyleSelector.OutOfBoundsStyle>
<Classes:OutOfBoundsRowStyleSelector.FilterValue>
<Binding Path="BoundsFilter" Mode="TwoWay"/>
</Classes:OutOfBoundsRowStyleSelector.FilterValue>
</Classes:OutOfBoundsRowStyleSelector>
Finally I added the RowStyleSelector to the DataGrid.
<telerik:RadGridView x:Name="dataGrid" ItemsSource="{Binding BatchList} RowStyleSelector="{StaticResource OutOfBoundsRowStyleSelector}" >
Unfortunately, you cannot use a binding like that. I got the following error message:
A 'Binding' cannot be set on the 'FilterValue' property of type 'OutOfBoundsRowStyleSelector'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
I added an extra property to the item model. Then whenever I refresh the grid, I fill in the value of the TextBox in each item. This way I can access the value through the item parameter from the GetStyle() method.
This doesn't seem like the most elegant way, but it did the job for me.
I have a GirdControl on my WPF that is bound to objects of type NoteFrontEnd. There is a property in NoteFrontEnd named NoteType that I want to use as the source of a visibility binding in a MenuItem.
The user must right-click on one of the NoteFrontEnd objects from the GridControl, and based on its NoteType property, either show or hide the MenuItem with Header="Process Item".
The GridControl and MenuItem are defined in xaml as:
<dxg:GridControl Name="GridCtrl"
ItemsSource="{Binding Path=BaseDashboardDataSource}"
SelectedItems="{Binding SelectedItems, Mode=TwoWay}"
AutoGenerateColumns="None">
<dxg:GridControl.Columns>
...
<dxg:GridColumn x:Name="NoteType" FieldName="NoteType" Header="Type" />
</dxg:GridControl.Columns>
<dxg:GridControl.View>
<dxg:TableView.ContextMenu>
<ContextMenu>
<ContextMenu.ItemsSource>
<CompositeCollection>
...
<!--Menu Item to toggle visibility of-->
<MenuItem Header="Process Item"
Visibility="{Binding ElementName=GridCtrl, Path=SelectedItem, Converter={StaticResource GVItemToVis}}"
Command="{...}">
</MenuItem>
</CompositeCollection>
</ContextMenu.ItemsSource>
</ContextMenu>
</dxg:TableView.ContextMenu>
</dxg:TableView>
</dxg:GridControl.View>
</dxg:GridControl>
My ViewModel is defined as such:
public class NoteViewModel : DashboardViewModelBase
{
...
public ObservableCollection<NoteFrontEnd> BaseDashboardDataSource { get; private set; }
public ObservableCollection<BrokerNoteFrontEnd> SelectedItems
{
get { return _selectedItems; }
set
{
if (_selectedItems == value) return;
_selectedItems = value;
OnPropertyChanged();
}
}
public NoteViewModel(...) {
...
BaseDashboardDataSource = new ObservableCollection<NoteFrontEnd>();
}
}
And NoteFrontEnd as:
public class NoteFrontEnd
{
public string NoteType { get; set; }
}
I am getting the below error though:
Cannot find source for binding with reference 'ElementName=GridCtrl'. BindingExpression:Path=SelectedItems;...
I have tried other bindings like the below but got the same error:
Visibility="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Grid}, Path=PlacementTarget.SelectedItem, Converter={StaticResource GVItemToVis}}"
How can I get this binding to work?
A context menu is a separate window. It is in a different namescope to your gridcontrol.
I suggest you:
Associate the context menu with the row each NoteFrontEnd is in rather than the entire grid.
NoteFrontEnd should be a viewmodel that implements inotifypropertychanged. Even if you have no changes you want to notify about. Always bind to an object implements inpc and avoid the possibility of a memory leak from bindings.
That then means you can get to the datacontext of that row using either the below or a variant:
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}"
That means the datacontext is pointed to the note which is templated into the row clicked.
You can then put a command in your NoteFrontEnd does whatever this is supposed to do.
You could also bind visibility of this menuitem to a property that is based on frontend and encapsulates whatever your logic is.
I suggest you (instead) consider using the canexecute part of icommand and return false when the user isn't supposed to click this option. That way the entry will still be there in the context menu but disabled and greyed.
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