Reputation: 5218
How can I add a ContextMenu to a ItemsControl, where:
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:\MyPath\RelayCommand.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.
Upvotes: 4
Views: 5462
Reputation: 13
It is a known bug in wpf and framework 4.0 try using 4.5 framework. https://connect.microsoft.com/VisualStudio/feedback/details/619658/wpf-virtualized-control-disconnecteditem-reference-when-datacontext-switch
Upvotes: -1
Reputation: 69985
Firstly, just check your parameter values for null
:
return parameter == null ? false : _canExecuteMethod((T)parameter);
Secondly, this is the old ContextMenu.DataContext
problem: The ContextMenu
is displayed in a different visual tree to the rest of the UI. It therefore has no access to a DataContext
from the main UI visual tree. Because of this, we have to use a little trick to pass it through to the other visual tree. Our connection between the two is the ContextMenu.PlacementTarget
property.
From the linked page, this property
Gets or sets the UIElement relative to which the ContextMenu is positioned when it opens.
We can use the Tag
property of the ContextMenu.PlacementTarget
object to pass the DataContext
. Basically, just set the Tag
property on the object that you will set the ContextMenu
on. Try something like this:
<ItemsControl x:Name="myItems" ItemsSource="{Binding MyItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding MyText}" Tag="{Binding DataContext,
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
ContextMenu="{StaticResource ItemContextMenu}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
...
<ContextMenu x:Key="ItemContextMenu" ItemsSource="{Binding ContextMenuItems}"
ItemContainerStyle="{StaticResource CommandMenuItemStyle}"
DataContext="{Binding PlacementTarget.Tag, RelativeSource={RelativeSource Self}}"/>
That's it. Now the UI elements declared in the ContextMenu
will have access to whatever object you data bind to the Tag
property. There's absolutely no need for EventSetter
s to use a ContextMenu
... it's quite simple if you know how.
Upvotes: 10