how to pass data when using MenuItem.ItemContainer

2020-08-09 05:58发布

问题:

i've been trying to have a dynamic ContextMenu to show the name property of each of the object in its collection of objects.
here is concrete example ,i'm connecting to a webservice to pull contacts and groups of a particular account.so i have those as global variables.i display the contacts in a listbox and i want to show on right click of a contact in the listbox the list of groups that it can be added to.
to be able to add a contact to a group i need the id of the contact(which i have) and the id of the group which i'm looking for here is my code.

xmlns:serviceAdmin="clr-namespace:MyWpfApp.serviceAdmin"
......
<ListBox.ContextMenu>
                        <ContextMenu>
                            <MenuItem Header="Refresh" Click="RefreshContact_Click"></MenuItem> 
                            <MenuItem Header="Add New Contact" Click="ContactNew_Click"></MenuItem>
                            <MenuItem Header="Add to Group" Name="groupMenus">
                                //<!--<MenuItem.Resources>
                                  //  <DataTemplate DataType="{x:Type serviceAdmin:groupInfo}" x:Key="groupMenuKey" > 
                                     //   <MenuItem>
                                     //       <TextBlock Text="{Binding name}" />
                                     //   </MenuItem>
                                   // </DataTemplate>

                               // </MenuItem.Resources>-->
                                <MenuItem.ItemContainerStyle>
                                    <Style>
                                        <Setter Property="MenuItem.Header" Value="{Binding name}"/>
                                        <Setter Property="MenuItem.Tag" Value="{Binding id}" />

                                    </Style>
                                </MenuItem.ItemContainerStyle>
                            </MenuItem>
                            <MenuItem Header="Delete Selected" Click="ContactDelete_Click"></MenuItem>
                        </ContextMenu>
                    </ListBox.ContextMenu>
                    ......

and on xaml.cs

//this code is in the method that loads the groups
loadedgroup = service.getGroups(session.key, null);
 groupListBox.ItemsSource = loadedgroup;
 groupMenus.ItemsSource = loadedgroup.ToList();

this code is showing the name of the groups alright but i need the id of the group clicked on.
If you've noticed i commented a portion of the xaml code. with that i could bind(with ease) the id to the tag.But it won't work and the MenuItem.ItemContainerStyle is the one working but then i'm lost:

Question 1 : how do i create a handler method for a click event of a submenu that has the names of the groups?
Question 2 : how do i get the clicked group id to work with?

thanks for reading and kindly help me in this

回答1:

Below is a sample using data binding. The data context of a sub menu items is an instance of Group.

XAML:

<Window x:Class="MenuTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="300" Width="300">
    <Grid Background="Red">
        <Grid.ContextMenu>
            <ContextMenu>
                <MenuItem Header="Menu Item 1"></MenuItem>
                <MenuItem Header="Menu Item 2"></MenuItem>
                <MenuItem Header="SubMenu" ItemsSource="{Binding Path=Groups}"
                          Click="OnGroupMenuItemClick">
                    <MenuItem.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Path=Name}" />
                        </DataTemplate>
                    </MenuItem.ItemTemplate>
                </MenuItem>
            </ContextMenu>
        </Grid.ContextMenu>
    </Grid>
</Window>

Code behind:

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;

namespace MenuTest
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            Groups = new List<Group>();
            Groups.Add(new Group() { Name = "Group1", Id = 1 });
            Groups.Add(new Group() { Name = "Group2", Id = 2 });
            Groups.Add(new Group() { Name = "Group3", Id = 3 });

            DataContext = this;
        }

        public List<Group> Groups { get; set; }

        private void OnGroupMenuItemClick(object sender, RoutedEventArgs e)
        {
            MenuItem menuItem = e.OriginalSource as MenuItem;
            Group group = menuItem.DataContext as Group;
        }
    }

    public class Group
    {
        public string Name { get; set;}
        public int Id { get; set;}
    }
}


回答2:

You can do this without using the Click event, or anything convoluted at all.

The trouble is that your Command has one DataContext (the ViewModel) and the item has another. Thus, two options: Either put the command on the items in ItemsSource (more flexible, since they can have different commands), or do a RelativeSource binding back up to the view, grab the ViewModel via view.DataContext, and get the command from that. This saves you some trivial hassle passing the command around to all those data items.

Note that since we're doing this via a Style, we'll be binding the same command to each menu item if we get the command from some DataContext that's common to all the similarly styled menu items. But since we're generating the items themselves from a list of like data items, that's probably what you want. If not, put commands on the data items.

If the items in your ItemsSource have a command, that's a public property of the data item which can easily be bound to MenuItem.Command in your ItemContainerStyle. More C#, less XAML.

This has the added benefit of working equally well on just one localized submenu in a larger menu system, where other menu items are defined in the conventional way. Here's a partial MRU list implementation. You could very easily do the same thing for other similar submenus -- or with very little further work, your entire main menu tree.

For simplicity, I assume the project has one namespace that's defined as local in XAML, and that this XAML is in a view called MainWindow.

Both of these have been tested in a complete implementation, but what's below is not complete: For the sake of a manageable SO answer, it's cut down to pretty much the minimum to enable the XAML to make sense. I ended up using the RelativeSource AncestorType version because it's a bit simpler and I don't need to give some of the list items different commands, but I kept the other version commented out.

<Window.Resources>
    <!-- ... -->
    <DataTemplate DataType="{x:Type local:MRUListItem}" >
        <Label Content="{Binding HeaderText}" />
    </DataTemplate>
    <!-- ... -->
</Window.Resources>

<!-- ... -->

<Menu>
    <MenuItem Header="_File">
        <MenuItem Header="_Save File" 
                  Command="{Binding SaveFileCommand}" />

        <!-- ... -->
        <!-- etc. -->
        <!-- ... -->

        <MenuItem Header="_Recent Files"
                  ItemsSource="{Binding MRUList}">
            <MenuItem.ItemContainerStyle>
                <Style TargetType="MenuItem">
                    <Setter Property="Command" 
                            Value="{Binding FileOpenCommand}" />
                    <Setter Property="CommandParameter" 
                            Value="{Binding FileName}" />
                </Style>
            </MenuItem.ItemContainerStyle>
        </MenuItem>

        <!-- etc. -->
    </MenuItem>

    <!-- ... -->
</Menu>

Alternative RelativeSource version, getting the command straight from the ViewModel in XAML:

<!--
            <MenuItem.ItemContainerStyle>
                <Style TargetType="MenuItem">
                    <Setter Property="Command" 
                        Value="{Binding RelativeSource={RelativeSource 
                        AncestorType={x:Type local:MainWindow}}, 
                        Path=DataContext.MRUFileOpenCommand}" />
                     <Setter Property="CommandParameter" 
                        Value="{Binding FileName}" />
                </Style>
            </MenuItem.ItemContainerStyle>
-->

C#

public class MRUList : ObservableCollection<MRUListItem>
{
    //  The owning ViewModel provides us with his FileOpenCommand
    //  initially. 
    public MRUList(ICommand fileOpenCommand)
    {
        FileOpenCommand = fileOpenCommand;
        CollectionChanged += CollectionChangedHandler;
    }

    public ICommand FileOpenCommand { get; protected set; }

    //  Methods to renumber and prune when items are added, 
    //  remove duplicates when existing item is re-added, 
    //  and to assign FileOpenCommand to each new MRUListItem. 

    //  etc. etc. etc. 
}

public class MRUListItem : INotifyPropertyChanged
{
    public ICommand FileOpenCommand { get; set; }

    private int _number;
    public int Number {
        get { return _number; }
        set
        {
            _number = value;
            OnPropertyChanged("Number");
            OnPropertyChanged("HeaderText");
        }
    }
    public String HeaderText { 
        get {
            return String.Format("_{0} {1}", Number, FileName);
        }
    }

    //  etc. etc. etc. 
}