wpf: Binding of nested Menu items - c#

Please note: This question is different to WPF - How can I create menu and submenus using binding because I want to use my own view models. MyFirstViewModel, MySecondViewModel, MyThirdViewModel cannot be merged in one kind of MenuItemViewModel and the layering with these 3 view models is my problem and the answer about hierarchical datatemplate doesn't work for me.
I want to make a menu where I know I have 3 levels.
The first level is one static menu item
the second level is generated from a binding to an ObservableCollection<MySecondViewModel> in my view model.
In MySecondViewModel I also have a ObservableCollection<MyThirdViewModel> which I want to bind to my 3rd menu item level.
In the third level I also want to use a template with a Checkbox which is also bind to properties in MyThirdViewModel. So my ViewModels look like this:
public class MyFirstViewModel
{
public ObservableCollection<MySecondViewModel> MenuItemsSecondLevel { get; set; }
...
}
public class MySecondViewModel
{
public string DisplayName{get; set;}
public ObservableCollection<MyThirdViewModel> MenuItemsThirdLevel{ get; set; }
...
}
public class MyThirdViewModel
{
public string DisplayName{get; set;}
public bool IsChecked {get;set;}
...
}
How can I create my WPF Menu based on this? if I try this:
<Menu>
<MenuItem Header="Select Source:" ItemsSource="{Binding MenuItemsSecondLevel}">
<MenuItem Header="{Binding DisplayName}" ItemsSource="{Binding MenuItemsThirdLevel}" >
<MenuItem.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding DisplayName}" IsChecked="{Binding IsChecked}"/>
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
</MenuItem>
</Menu>
Then my bindings are not working. He cannot find any of my Collections.If I make it more advanced like this:
<Menu>
<MenuItem Header="Select Source:" ItemsSource="{Binding MenuItemsSecondLevel}">
<MenuItem.ItemTemplate>
<DataTemplate>
<MenuItem Header="{Binding DisplayName}" ItemsSource="{Binding MenuItemsThirdLevel}" >
<MenuItem.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding DisplayName}" IsChecked="{Binding IsChecked}" />
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
</Menu>
He finds the second level but not the third. What's the best way to make the menu levels like the structure of my view models?
Please note, I know that you can make menu items selectable but there is a design reason why we use here checkboxes.

You could use this
<Menu>
<MenuItem Header="Select Source:"
ItemsSource="{Binding FirstViewModel.MenuItemsSecondLevel}">
<MenuItem.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:MySecondViewModel}"
ItemsSource="{Binding MenuItemsThirdLevel}">
<TextBlock Text="{Binding DisplayName}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:MyThirdViewModel}">
<CheckBox Content="{Binding DisplayName}" />
</DataTemplate>
</MenuItem.Resources>
</MenuItem>
</Menu>
Assuming FirstViewModel is a property of your viewmodel.

Related

WPF firing a view model Command from a list view item

There is a WPF MVVM app. On the main view I have a list of elements, which are defined with ListView.ItemTemplate, in that I want to have a context menu with Delete action.
The Command for that is separated from the view and is kept in ViewModel DreamListingViewModel.
The problem is that on clicking on Delete I can't get it to execute the command on ViewModelk as context there is that of the item, not the items container.
I can make it work somehow by moving the context menu definition outside of the list view elements, but then when I open the context menu, it flickers, as if it's being called "20" times (which what I think does happen, as many times as I have elements in collection), anyways, I need a clean solution for that and I am very bad with XAML.
Here is how my View looks:
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="0 5 0 5" Background="Transparent" Width="auto">
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Delete"
Command="{Binding DeleteSelectedDream}"
CommandParameter="{Binding DeleteSelectedDream,
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type viewmodels:DreamListingViewModel}}}"
/>
</ContextMenu>
</Grid.ContextMenu>
...
It's the main window and initialized in a generic host in App.cs:
public partial class App : Application
{
private readonly IHost _host;
public App()
{
...
_host = Host.CreateDefaultBuilder().ConfigureServices(services =>
{
...
services.AddTransient<DreamListingViewModel>();
services.AddSingleton((s) => new DreamListingView()
{
DataContext = s.GetRequiredService<DreamListingViewModel>()
});
...
}).Build();
The Command and CommandParameter values are what I've been experimenting with, but it doesn't work
Here is how my ViewModel looks:
internal class DreamListingViewModel : ViewModelBase
{
public ICommand DeleteSelectedDream{ get; }
...
Finally, when the command is fired, I need to pass the current element on which the menu has been shown.
So, here is what I want:
User clicks on a list item with mouse right button - OK
Sees a menu with Delete entry - OK
On Delete click, Command DeleteSelectedDream is fired with current dream (item in the list) as a parameter - ERR
Your example is somewhat lacking necessary information, but I'll try to help.
First you need to verify that you are actually bound to your view model. Are you using Prism or just standard WPF ? In the constructor of your code-behind of your view, set up the DataContext to an instance of your VM.
InitializeComponent();
this.DataContext = new DreamListingViewModel();
Now, you bind to a relative source via Mode 'FindAncestor' and the AncestorType is set to the type of a view model. That usually won't work, as the view model is not naturally a part of the visual tree of your WPF view. Maybe your ItemTemplate somehow wires it up. In a large WPF app of mine I use Telerik UI for WPF and a similar approach to you, however, I set up the DataContext of the Context menu to a RelativeSource set to Self combined with Path set to PlacementTarget.DataContext.
You do not have to use all the XAML in my example, just observe how I do it. Exchange 'RadContextMenu' with 'ContextMenu', Ignore the Norwegian words - here and only use what you need :
<telerik:RadContextMenu x:Key="CanceledOperationsViewContextMenu" DataContext="{Binding RelativeSource={RelativeSource Mode=Self}, Path=PlacementTarget.DataContext, UpdateSourceTrigger=PropertyChanged}">
<MenuItem Header="{Binding PatientName}" IsEnabled="False" Style="{StaticResource ContextMenuHeading}" />
<MenuItem Header="Gå til aktuell SomeAcme-liste" IsEnabled="{Binding IsValid}" Command="{Binding NavigateToListCommand}" />
<MenuItem Header="Åpne protokoll..." Command="{Binding CommonFirstProtocolCommand, Mode=OneWay}" CommandParameter="{Binding}" />
<MenuItem Header="Åpne Opr.spl.rapport...." Command="{Binding CommonFirstNurseReportCommand, Mode=OneWay}" CommandParameter="{Binding}" />
</telerik:RadContextMenu>
In your example it will be :
<ContextMenu x:Key="SomeContextMenu" DataContext="{Binding RelativeSource={RelativeSource Mode=Self}, Path=PlacementTarget.DataContext, UpdateSourceTrigger=PropertyChanged}">
<MenuItem Header="Delete" />
Command="{Binding DeleteSelectedDream}"
CommandParameter="{Binding DeleteSelectedDream,
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListViewItem}}}"
/>
</telerik:RadContextMenu>
Now I here consider you are using the class ListViewItem
https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.listviewitem?view=netframework-4.8
It might be that you need to specify DataContext.DeleteSelectedDream here to be sure you bind up to the DataContext where your implementation of ICommand is.
Accidentally found this answer, that's basically what I needed, just added to it a CommandParameter to send the item and it works like magic!
<ListView Name="lvDreams" ItemsSource="{Binding Dreams}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="0 5 0 5" Background="Transparent" Width="auto">
<Grid.ContextMenu>
<ContextMenu>
<MenuItem
Header="Delete"
Command="{Binding DataContext.DeleteSelectedDream, Source={x:Reference lvDreams}}"
CommandParameter="{Binding}"
/>
</ContextMenu>
</Grid.ContextMenu>
...
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I find the following the simplest; perhaps it's because I do not understand WPF, but it's "simple" to remember, and it works with my MVVM pattern.
<ListBox ItemsSource="{Binding MyViewModelItemsCollection, Mode=OneWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="Transparent" >
<TextBlock Text="{Binding Path=Name, Converter={StaticResource FullPathToFileName}, Mode=OneWay}" Grid.Column="0">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem
Command="{Binding Path=DataContext.MyViewModelAction, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
Header="{Binding Name, Converter={StaticResource resourceFormat}, ConverterParameter={x:Static res:Resources.CONTEXT_MENU_BLOCK_APPLICATION}}">
</MenuItem>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</grid>
</DataTemplate>
</ListBox.ItemTemplate
</ListBox>
The MyViewModelXXXXXXX named items are in the view model that is mapped to the data context of the control.

Xamarin Forms Bind command outside ItemSource

I have a problem. I created this ListView:
<ListView ItemsSource="{Binding knownDeviceList}" SelectionMode="None" RowHeight="90" ItemTapped="device_Clicked">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.ContextActions>
<MenuItem Command="{Binding DeleteDevice}"
CommandParameter="{Binding Id}"
Text="Delete" IsDestructive="True" />
</ViewCell.ContextActions>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The ListView is bound to a List with objects in it, but I need to bind the command to an ICommand outside the List on root level of the ViewModel. How can I do that, because now the ICommand doesn't get triggered when I try to remove an item from the List!
Here is my Command in my ViewModel:
public ICommand DeleteDevice
{
get
{
return new Command<int>((x) => RemoveDevice_Handler(x));
}
}
What am I doing wrong?
Your MenuItem.BindingContext is scoped to the actual item in that cell, not the view model of the whole page (or ListView). You will either need to tell the binding that it needs to looks else where, like this:
<ListView x:Name="MyListView">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.ContextActions>
<MenuItem Command="{Binding Path=BindingContext.DeleteDevice, Source={x:Reference MyListView}}}"/>
</ViewCell.ContextActions>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Note that I removed the attributes that you have in there just to make clear which ones I added. You can keep them in, this is just for readability.
Or, you can use the new Relative Bindings. Then you would implement the command binding like this:
Command="{Binding Source={RelativeSource AncestorType={x:Type local:YourViewModelClass}}, Path=DeleteDevice}"
The Context that you are trying to bind with from the Command is not the Page's one but the ItemSource, This is why you can simply bind the Id to the CommandParameter.
To fix this, you need to target the page's BindingContext since your Command lives at the ViewModel root level. You can achieve it by adding a x:Name property to your ListView and target the right Context through it.
Here the fix:
<ListView x:Name="listView" ItemsSource="{Binding knownDeviceList}" SelectionMode="None" RowHeight="90" ItemTapped="device_Clicked">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell>
<ViewCell.ContextActions>
<MenuItem Command = "{Binding BindingContext.DeleteDevice, Source={x:Reference listView}}"
CommandParameter="{Binding Id}"
Text="Delete" IsDestructive="True" />
</ViewCell.ContextActions>
</ViewCell>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
Hope it helps & happy coding!

Bind contextMenu to a different viewmodel from treeview

I want to show a contextMenu item when I am using rightclick on a treeview item.
After that, I want to use a command when I click on my MenuItem, but I need to bind the command with a different viewmodel and the command parameter with the good viewmodel who come from my treeview selected item.
So for the moment, I have something like that :
<TreeView x:Name="TreeViewProtocolsAndEquipments" AllowDrop="True"
ItemsSource="{Binding ModuleParams}">
<TreeView.Resources>
<!-- CONTEXT MENU -->
<!-- Protocol -->
<ContextMenu x:Key="ContextMenuProtocol">
<MenuItem Header="Add new equipment" Command="{Binding AddNewEquipmentCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}">
<MenuItem.Icon>
<Image Source="Images/Add.png" />
</MenuItem.Icon>
</MenuItem>
<Separator />
</ContextMenu>
<!-- MODULE XXX -->
<!-- ModuleParam > xxx -->
<HierarchicalDataTemplate DataType="{x:Type xxx:ModuleParamXXXViewModel}" ItemsSource="{Binding ModuleItems}">
<TextBlock Text="XXX" Foreground="Green" ContextMenu="{StaticResource ContextMenuProtocol}"/>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
For the moment my command is bind to xxx:ModuleParamXXXViewModel if I just let { binding }
Can I bind my Command to my ActivatedProtocolsAndEquipmentsTreeViewModel (the datacontext of this usercontrol) and keep on the CommandParameter my xxx:ModuleParamXXXViewModel (who is the Item from the treeview where we triggered the right click to show the contextMenu) ?
How can I achieve this in an other way with MVVM practice ?
I also tried to use this but it didn't work too :
<MenuItem Header="Add new equipment" Command="{Binding Path=DataContext.AddNewEquipmentCommand, Source={x:Reference TreeViewProtocolsAndEquipments}}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}">
And with this i get Object Reference not set to an instance of an object
The UserControl is not a visual ancestor of the MenuItem since a ContextMenu resides in its own visual tree.
Bind the Tag property of the TextBlock to the UserControl and then bind the Command property to the PlacementTarget of the ContextMenu:
<TreeView x:Name="TreeViewProtocolsAndEquipments" AllowDrop="True"
ItemsSource="{Binding ModuleParams}">
<TreeView.Resources>
<!-- CONTEXT MENU -->
<!-- Protocol -->
<ContextMenu x:Key="ContextMenuProtocol">
<MenuItem Header="Add new equipment"
Command="{Binding PlacementTarget.Tag.DataContext.AddNewEquipmentCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}"
CommandParameter="{Binding}">
<MenuItem.Icon>
<Image Source="Images/Add.png" />
</MenuItem.Icon>
</MenuItem>
<Separator />
</ContextMenu>
<!-- MODULE XXX -->
<!-- ModuleParam > xxx -->
<HierarchicalDataTemplate DataType="{x:Type xxx:ModuleParamXXXViewModel}" ItemsSource="{Binding ModuleItems}">
<TextBlock Text="XXX" Foreground="Green"
Tag="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}"
ContextMenu="{StaticResource ContextMenuProtocol}"/>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>

MemoryManagement for ObservableCollections in .Net

I am working on a WPF application in which I am working with an ObservableCollection of CustomObject
public ObservableCollection<ProjectsToShow> Projects{get;set;}
Definition for ProjectsToShow class
public class ProjectsToShow
{
public ProjectsToShow()
{
Wells = new ObservableCollection<WellsToShow>();
}
public Project ProjectObject { get; set; }
ObservableCollection<WellsToShow> _wells;
public ObservableCollection<WellsToShow> Wells{get;set;}
}
This class initialize a collection for WellsToShow whose definition is
public class WellsToShow
{
public WellsToShow()
{
Datasets = new ObservableCollection<DatasetsToShow>();
}
public Well WellObject { get; set; }
ObservableCollection<DatasetsToShow> _datasets;
public ObservableCollection<DatasetsToShow> Datasets
{
get { return _datasets; }
set
{
_datasets = value;
NotifyPropertyChanged("Datasets");
}
}
}
and one more level like this.
Now I am profiling my application using memory profiler and it keeps adding the objects in the collection to the memory. I was expecting that calling
Projects.Clear();
will release all the objects from the memory but it does not work that way. Even I try to set the Projects to null but even that did not work. Objects of WellsToShow and DatasetToShow still hold on to memory. So for testing purpose I try this code
foreach(var project in MainViewModel.Projects)
{
foreach(var well in project.Wells)
{
well.Datasets.Clear();
}
project.Wells.Clear();
}
MainViewModel.Projects.Clear();
As per memory profiler they are not in the memory anymore. For the record, each time I run the profiler it runs GC.Collect first and then do the profiling.
Can some please explain how this thing works. If this is the correct way to clear the collection then I need to run and fix this thing in all the projects.
Update 1, Binding this collection to view
I am binding my Project property to TreeView control and upon investigating it profiler. This control is keeping hold of my collection, I believed clearing the items source collection should do the job but this is not the case here.
<TreeView VirtualizingStackPanel.IsVirtualizing="False" ItemsSource="{Binding Projects}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type bll:ProjectsToShow}" ItemsSource="{Binding Wells}">
<TextBlock HorizontalAlignment="Stretch" Text="{Binding ProjectName}" x:Name="TextBlockProject" Tag="{Binding DataContext,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Window}}">
<TextBlock.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.Tag, RelativeSource={RelativeSource Self}}" >
<MenuItem
Command="{Binding FileEditProjectCommand}">
<MenuItem.Header>
<TextBlock Text="{DynamicResource EditProject}"/>
</MenuItem.Header>
</MenuItem>
<MenuItem Command="{Binding FileDeleteProjectCommand}">
<MenuItem.Header>
<TextBlock Text="{DynamicResource DeleteProject}"/>
</MenuItem.Header>
</MenuItem>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type bll:WellsToShow}" ItemsSource="{Binding Datasets}">
<TextBlock HorizontalAlignment="Stretch" Text="{Binding WellName}" Tag="{Binding DataContext,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Window}}">
<TextBlock.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
<MenuItem Command="{Binding FileEditWellCommand}">
<MenuItem.Header>
<TextBlock Text="{DynamicResource EditWell}"/>
</MenuItem.Header>
</MenuItem>
<MenuItem Command="{Binding FileDeleteWellCommand}">
<MenuItem.Header>
<TextBlock Text="{DynamicResource DeleteWell}"></TextBlock>
</MenuItem.Header>
</MenuItem>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</HierarchicalDataTemplate>
Update 2 Screenshot attached
If the visual objects corresponding to the object that are (were) in the collection, those visual objects will hold onto those references within their respective DataContexts. Make sure to run the memory profile only AFTER the objects have visually cleared.
I also recommend Snoop for any WPF debugging work, as this may be able to show your objects and what's referencing them.

Tree View Context Menu - Pass selected item to command?

I'm wondering how to pass the selected item to a command from a treeview / HierarchicalDataTemplate ?
Here is the code that I have so far, it displays the context menu but I haven't bound the commands to it yet. The command binding is the easy part, but how do I tell which node it came from ?
<HierarchicalDataTemplate
DataType="{x:Type viewModel:UsersViewModel}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="Images\Region.png" />
<TextBlock Text="{Binding UserName}">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="Edit" />
<MenuItem Header="Delete"/>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
Just {Binding} should be the whole item.
(To pass it to the Command bind the CommandParameter, in Execute and CanExecute it will become the method parameter (which you then need to cast to your item-type))

Categories

Resources