Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
250 views
in Technique[技术] by (71.8m points)

c# - Bind Context Menu inside ItemsControl?

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 EventSetters to use a ContextMenu... it's quite simple if you know how.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...