How can I add a ContextMenu to a ItemsControl, where:
- The ItemsSource of the ItemsControl is in the ViewModel of the UserControl holding the ItemsControl
- The CommandParameter of the Context Menu is the ViewModel binded to the Item in the ItemsControl.
I followed this approach. However, I have a Command that Removes Items from the ObservableCollection binded to my ItemsControl. When this happen an exception is thrown inside the RelayCommand. It seems to me as the ContextMenu is not "hiding", so it tries to evaluate the "CanExecute" for its commands, but as the item has been removed it can not cast the parameter to "T" in the CanExecute method of the RelayCommand Class.
I would like to know how is the correct way to accomplish what I need.
My Implementation so far:
MainViewModel
public class MainViewModel
{
public ObservableCollection<MyContextMenuClass> ContextMenuItems{ get;set; }
public ObservableCollection<MyItemClass> MyItems{ get;set; }
public void AddItem(MyItemClass item)
{
MyItems.Add(item);
}
public void AddContextMenuItem(MyContextMenuClass item)
{
ContextMenuItems.Add(item);
}
public MainViewModel(IList<MyItemClass> myItems, IList<MyContextMenuClass> myContextualMenuItems)
{
MyItems.AddRange(myItems);
ContextMenuItems.AddRange(myContextualMenuItems);
}
public MainViewModel()
{}
}
MyItemClass
public class MyItemClass
{
public string MyText{get;set;}
}
MyContextMenuClass
public class MyContextMenuClass
{
public RecentContextMenuItem()
{}
public string Caption{get;set;}
public RelayCommand<MyItemClass> Command{get;set;}
}
My UserControl (DataContext = MainViewModel)
<UserControl x:Class="MyNamespace.MyUserControl"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<Style x:Key="CommandMenuItemStyle" TargetType="{x:Type MenuItem}" BasedOn="{StaticResource {x:Type MenuItem}}">
<Setter Property="MenuItem.Header" Value="{Binding Caption}" />
<Setter Property="MenuItem.Command" Value="{Binding Command}" />
<Setter Property="MenuItem.CommandParameter" Value="{Binding PlacementTarget.DataContext,
RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
</Style>
<ContextMenu x:Key="ItemContextMenu" ItemsSource="{Binding ContextMenuItems}"
ItemContainerStyle="{StaticResource CommandMenuItemStyle}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"/>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Title}" Margin="20,5,0,5" Foreground="#FF5D5858" FontFamily="Courier" FontSize="15" Grid.ColumnSpan="2" FontWeight="SemiBold"></TextBlock>
<ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="2" Padding="5,0,0,0">
<ItemsControl x:Name="myItems" ItemsSource="{Binding MyItems}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding MyText}" /> <!--Simplied this for the example-->
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<EventSetter Event="ContextMenu.ContextMenuOpening" Handler="Item_ContextMenuOpening"></EventSetter>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</ScrollViewer>
</Grid>
My UserControl Codebehind
public partial class MyUserControl : UserControl
{
/// <summary>
/// Initializes a new instance of the <see cref="RecentView"/> class.
/// </summary>
public MyUserControl()
{
InitializeComponent();
}
private void Item_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
var contentPresenter = sender as ContentPresenter;
if (contentPresenter != null)
{
this.Dispatcher.BeginInvoke(new Action<ContentPresenter>(ShowItemContextMenu), new object[] { contentPresenter });
}
}
private void ShowItemContextMenu(ContentPresenter sourceContentPresenter)
{
if (sourceContentPresenter != null)
{
var ctxMenu = (ContextMenu)this.FindResource("ItemContextMenu");
ctxMenu.DataContext = this.DataContext;
if (ctxMenu.Items.Count == 0)
{
sourceContentPresenter.ContextMenu = null;
}
else
{
ctxMenu.PlacementTarget = sourceContentPresenter;
ctxMenu.IsOpen = true;
}
}
}
}
The RemoveItemCommand I add to the MainViewModel
new RelayCommand<MyItemClass>(RemoveItem, (param) => true);
private void RemoveItem(MyItemClassitemToRemove)
{
MyItems.Remove(itemToRemove);
}
The CanExecute method of the RelayCommand
public bool CanExecute(object parameter)
{
if (_canExecute == null)
{
return true;
}
if (parameter == null)
{
return _canExecute.Invoke(default(T));
}
T value;
try
{
value = (T)parameter;
}
catch(Exception exception)
{
Trace.TraceError(exception.ToString());
return _canExecute.Invoke(default(T));
}
return _canExecute.Invoke(value);
}
I get the error in the value = (T)parameter; line, because the parameter is Disconnected and can't cast it to T.
The Exception I get:
MyProgram.vshost.exe Error: 0 : System.InvalidCastException: Unable to cast object of type 'MS.Internal.NamedObject' to type 'MyItemClass'.
at MyNamespace.RelayCommand`1.CanExecute(Object parameter) in c:MyPathRelayCommand.cs:line xxx
If I inspect the parameter it is a NamedObject:
- parameter {DisconnectedItem} object {MS.Internal.NamedObject}
- Non-Public members
_name "{DisconnectedItem}" string
The problem is not the Exception, is the fact that it reaches this point with a DisconnectedItem. This get evaluated multiple times. It is like the ContextMenu remains "forever" in the Visual Tree.
See Question&Answers more detail:
os