WPF TreeView - XAML ContextMenu 可以基于属性有条件吗?

问题描述

我使用 TreeView显示对象层次结构,使用 Philipp Sumi's method 来组织使用转换器的异构数据。这有效。

但是,我现在想在 XAML 中添加 ContextMenus,这通常特定于用户单击的对象类型。由于使用 FolderItem 来表示多个类,因此多个对象类型可以共享相同的 <ContextMenu> 定义。

下面的示例显示一个基本的 TreeView。猫和狗具有相同的 <ContextMenu> 定义。我能否更具体地定位 ContextMenu,以便仅在用户点击“Dogs”而不是“Cats”时显示“Walk all dogs”菜单

我正在寻找类似于针对 NameFolderItem 属性内容(即逻辑为 [display context menu] if Name == "Dogs"。)

我当然可以使用带有右键单击事件的代码隐藏来实现此功能,并且到目前为止已经完成。只是以良好实践的名义尝试在 XAML 中做更多事情。

<Window x:Class="TestTreeView.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:TestTreeView"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
    
    <Window.Resources>
        <local:SimpleFolderConverter x:Key="folderConverter" />

        <HierarchicalDataTemplate DataType="{x:Type local:Pets}">
            <HierarchicalDataTemplate.ItemsSource>
                <MultiBinding Converter="{StaticResource folderConverter}" 
                      ConverterParameter="Cats,Dogs">
                    <Binding Path="cats" />
                    <Binding Path="dogs" />
                </MultiBinding>
            </HierarchicalDataTemplate.ItemsSource>
            <TextBlock Text="{Binding Path=description}" />
        </HierarchicalDataTemplate>

        <DataTemplate DataType="{x:Type local:Cat}">
            <TextBlock Text="{Binding Path=Name}" />
        </DataTemplate>

        <DataTemplate DataType="{x:Type local:Dog}">
            <TextBlock Text="{Binding Path=Name}" />
        </DataTemplate>

        <!-- data template for FolderItem instances -->
        <HierarchicalDataTemplate DataType="{x:Type local:FolderItem}" ItemsSource="{Binding Path=Items}">

            <StackPanel Orientation="Horizontal">
                
                <StackPanel.ContextMenu>
                    <ContextMenu> <!-- This applies to more than one type of underlying object -->
                        <MenuItem Header="Walk all dogs"/>
                    </ContextMenu>
                </StackPanel.ContextMenu>

                <TextBlock Text="{Binding Path=Name}" />

            </StackPanel>
        </HierarchicalDataTemplate>
    </Window.Resources>

    <Grid>
        <TreeView x:Name="treeView"/>
    </Grid>
</Window>

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Pets pets = new Pets();
        pets.cats.Add(new Cat());
        pets.dogs.Add(new Dog());
        treeView.ItemsSource = new ObservableCollection<Pets>() { pets };

    }
}
public class Pets {
    public string description { get; set; } = "Pets";
    public ObservableCollection<Cat> cats { get; set; } = new ObservableCollection<Cat>();
    public ObservableCollection<Dog> dogs { get; set; } = new ObservableCollection<Dog>();
    public IEnumerable<Pets> CollectionOfSelf
    {
        get { yield return this; }
    }
}
public class Cat {
    public string Name { get; set; } = "Socks";
}
public class Dog {
    public string Name { get; set; } = "Fido";
}
public class FolderItem
{
    #region Name

    /// <summary>
    /// The name that can be displayed or used as an ID to perform more complex styling.
    /// </summary>
    private string name;


    /// <summary>
    /// The name that can be displayed or used as an ID to perform more complex styling.
    /// </summary>
    public string Name
    {
        get { return name; }
        set
        {
            //ignore if values are equal
            if (value == name) return;

            name = value;
            OnPropertyChanged("Name");
        }
    }

    private void OnPropertyChanged(string v)
    {
        //
    }

    #endregion

    #region Items

    /// <summary>
    /// The child items of the folder.
    /// </summary>
    private IEnumerable items;


    /// <summary>
    /// The child items of the folder.
    /// </summary>
    public IEnumerable Items
    {
        get { return items; }
        set
        {
            //ignore if values are equal
            if (value == items) return;

            items = value;
            OnPropertyChanged("Items");
        }
    }

    #endregion

    public FolderItem()
    {
    }

    /// <summary>
    /// This method is invoked by WPF to render the object if
    /// no data template is available.
    /// </summary>
    /// <returns>Returns the value of the <see cref="Name"/>
    /// property.</returns>
    public override string ToString()
    {
        return string.Format("{0}: {1}",GetType().Name,Name);
    }
}

public class SimpleFolderConverter : IMultiValueConverter
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="values"></param>
    /// <param name="targettype"></param>
    /// <param name="parameter"></param>
    /// <param name="culture"></param>
    /// <returns></returns>
    public object Convert(object[] values,Type targettype,object parameter,CultureInfo culture)
    {
        //get folder name listing...
        string folder = parameter as string ?? "";
        var folders = folder.Split(',').Select(f => f.Trim()).ToList();
        //...and make sure there are no missing entries
        while (values.Length > folders.Count) folders.Add(String.Empty);

        //this is the collection that gets all top level items
        List<object> items = new List<object>();

        for (int i = 0; i < values.Length; i++)
        {
            //make sure were working with collections from here...
            IEnumerable childs = values[i] as IEnumerable ?? new List<object> { values[i] };

            string folderName = folders[i];
            if (folderName != String.Empty)
            {
                //create folder item and assign children
                FolderItem folderItem = new FolderItem { Name = folderName,Items = childs };
                items.Add(folderItem);
            }
            else
            {
                //if no folder name was specified,move the item directly to the root item
                foreach (var child in childs) { items.Add(child); }
            }
        }

        return items;
    }

    public object[] ConvertBack(object value,Type[] targettypes,CultureInfo culture)
    {
        throw new NotSupportedException("Cannot perform reverse-conversion");
    }

}

解决方法

我做过这样的事情。当用户选择一个项目时,我有一个名为 SelectedItem 的所选项目的属性。然后我可以使用基于 SelectedItem 属性的触发器来个性化 ContextMenuMenuItems。特别是,我使用 SelectedItem 的属性来确定禁用哪个菜单项,但您也可以控制每个菜单项的可见性。 或者,虽然我还没有尝试过,但应该可以使用设置 ContextMenu 属性的触发器。

,

如果我正确理解了您的问题,您可以根据 ContextMenuName 使用带有 FolderItemStyle 应用不同的 DataTrigger

<HierarchicalDataTemplate DataType="{x:Type local:FolderItem}" ItemsSource="{Binding Path=Items}">

    <StackPanel Orientation="Horizontal">
        <StackPanel.Style>
            <Style TargetType="StackPanel">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Name}" Value="Dogs">
                        <Setter Property="ContextMenu">
                            <Setter.Value>
                                <ContextMenu>
                                    <MenuItem Header="Walk all dogs"/>
                                </ContextMenu>
                            </Setter.Value>
                        </Setter>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Name}" Value="Cats">
                        <Setter Property="ContextMenu">
                            <Setter.Value>
                                <ContextMenu>
                                    <MenuItem Header="Walk all cats"/>
                                </ContextMenu>
                            </Setter.Value>
                        </Setter>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </StackPanel.Style>

        <TextBlock Text="{Binding Path=Name}" />

    </StackPanel>
</HierarchicalDataTemplate>