混合动态和静态XAML菜单项

6

我有一个场景,需要同时拥有静态和动态菜单项。静态项目将在XAML中定义,而动态项目则由视图模型提供。每个动态项目本身将由一个视图模型表示,我们称之为CommandViewModel。

CommandViewModel除其他内容外,还具有显示名称,并且可以包含其他CommandViewModels。

用作菜单数据上下文的MainViewModel如下:

public class MainMenuViewModel : INotifyPropertyChanged
{

  private ObservableCollection<CommandViewModel> m_CommandVMList;


  public MainMenuViewModel()
  {
    m_ CommandVMList = new ObservableCollection<CommandViewModel>();

    CommandViewModel cmv = new CommandViewModel();
    cmv.DisplayName = "Dynamic Menu 1";
    m_CommandVMList.Add(cmv);

    cmv = new CommandViewModel();
    cmv.DisplayName = "Dynamic Menu 2";
    m_CommandVMList.Add(cmv);

    cmv = new CommandViewModel();
    cmv.DisplayName = "Dynamic Menu 3";
    m_CommandVMList.Add(cmv);

  }

  public ObservableCollection<CommandViewModel> CommandList
  {
    get { return m_CommandVMList; }
    set
    {
      m_CommandVMList = value;
      OnPropertyChanged("CommandList");
    }
  }

菜单的XAML代码:

<Grid>
  <Grid.Resources>
    <HierarchicalDataTemplate DataType="{x:Type Fwf:CommandViewModel}" ItemsSource="{Binding Path=CommandViewModels}">
      <MenuItem Header="{Binding Path=DisplayName}"/>
    </HierarchicalDataTemplate>
  </Grid.Resources>

  <Menu VerticalAlignment="Top" HorizontalAlignment="Stretch">
    <MenuItem Header="Static Top Menu Item 1">
      <MenuItem Header="Static Menu Item 1"/>
        <MenuItem Header="Static Menu Item 2"/>
        <MenuItem Header="Static Menu Item 3"/>
        <ItemsControl ItemsSource="{Binding Path= CommandList}"/>
        <MenuItem Header="Static Menu Item 4"/>
      </MenuItem>
  </Menu>
</Grid>

除了一个问题,所有东西都很好,就是无论我尝试如何表示动态菜单列表,比如使用ItemsControl,在UI上它都会显示为一个菜单项包含更多的菜单项,所以当你点击这个项目时,整个动态菜单项集合都会被选中。该集合在表示上是正确的,因为每个动态菜单项本身都显示为一个菜单项,但在这个更大的菜单项内部。我认为我知道原因了,因为菜单只是为其中包含的每个项(静态或动态)创建菜单项,而不关心它们的类型。是否有一种方法可以使每个动态菜单项在示例中与静态菜单项一样属于父菜单项并在同一级别上创建?
1个回答

5

不要在XAML中硬编码您的“静态”菜单项,而是在VM侧将其硬编码为CommandViewModel对象。

既然您无论如何都在硬编码它,那么您不会失去灵活性,并且如果您选择在将来以不同的方式呈现它们,则可以使您的静态菜单项与HierarchicalDataTemplate保持同步,从而获得额外的好处。

请注意,您可能需要更改绑定,以使您的菜单绑定到菜单项的集合。 您可以在此处找到一个示例。

编辑:代码示例

我能够相当快地对此进行修改,大多数类定义都不完整(例如INotifyPropertyChanged),但它应该能够让您了解可以做些什么。 我在第三个命令上添加了一些命令嵌套,以确保分层数据模板正常工作。

以下是XAML:

<Window
    x:Class="WPFDynamicMenuItems.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WPFDynamicMenuItems"
    Title="Window1" Height="300" Width="600">
    <Grid>
        <Grid.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:CommandViewModel}" ItemsSource="{Binding Path=CommandList}">
                <ContentPresenter
                    Content="{Binding Path=DisplayName}"
                    RecognizesAccessKey="True" />
            </HierarchicalDataTemplate>
        </Grid.Resources>
        <ToolBarTray>
            <ToolBar>
            <Menu>
                <Menu.ItemsSource>
                    <CompositeCollection>
                        <MenuItem Header="A"></MenuItem>
                        <MenuItem Header="B"></MenuItem>
                        <MenuItem Header="C"></MenuItem>

                        <CollectionContainer x:Name="dynamicMenuItems">
                        </CollectionContainer>

                        <MenuItem Header="D"></MenuItem>

                    </CompositeCollection>
                </Menu.ItemsSource>

            </Menu>
                </ToolBar>
        </ToolBarTray>
    </Grid>
</Window>

以下是代码的后端:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;

namespace WPFDynamicMenuItems
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        private MainMenuViewModel _mainMenuVM = new MainMenuViewModel();

        public Window1()
        {
            InitializeComponent();

            this.dynamicMenuItems.Collection = this._mainMenuVM.CommandList;
        }
    }


    public class MainMenuViewModel : INotifyPropertyChanged
    {
        private ObservableCollection<CommandViewModel> m_CommandVMList;

        public MainMenuViewModel()
        {
            m_CommandVMList = new ObservableCollection<CommandViewModel>();
            CommandViewModel cmv = new CommandViewModel();
            cmv.DisplayName = "Dynamic Menu 1";
            m_CommandVMList.Add(cmv);
            cmv = new CommandViewModel();
            cmv.DisplayName = "Dynamic Menu 2";
            m_CommandVMList.Add(cmv);
            cmv = new CommandViewModel();
            cmv.DisplayName = "Dynamic Menu 3";
            m_CommandVMList.Add(cmv);

            CommandViewModel nestedCMV = new CommandViewModel();
            nestedCMV.DisplayName = "Nested Menu 1";
            cmv.CommandList.Add(nestedCMV);

            nestedCMV = new CommandViewModel();
            nestedCMV.DisplayName = "Nested Menu 2";
            cmv.CommandList.Add(nestedCMV);
        }
        public ObservableCollection<CommandViewModel> CommandList
        {
            get { return m_CommandVMList; }
            set { m_CommandVMList = value; OnPropertyChanged("CommandList"); }
        }

        protected void OnPropertyChanged(string propertyName)
        {
            //  Hook up event...
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion
    }

    public class CommandViewModel : INotifyPropertyChanged
    {
        private ObservableCollection<CommandViewModel> m_CommandVMList;

        public CommandViewModel()
        {
            this.m_CommandVMList = new ObservableCollection<CommandViewModel>();
        }

        public string DisplayName { get; set; }

        public ObservableCollection<CommandViewModel> CommandList
        {
            get { return m_CommandVMList; }
            set { m_CommandVMList = value; OnPropertyChanged("CommandList"); }
        }

        protected void OnPropertyChanged(string propertyName)
        {
            //  Hook up event...
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion
    }
}

是的,我希望我能够这样做,但不幸的是我不能,主要原因是 XAML 几乎完全不受我的控制。它由应用程序开发人员创建/管理,他们只是使用我的模型来增强他们的菜单,以便从代码中驱动需要的内容。 - Adrian
很不幸。您是否有任何方法可以更改XAML以利用CompositeCollection(http://msdn.microsoft.com/en-us/library/system.windows.data.compositecollection.aspx)?这并不理想,但它可能允许您注入动态项。我唯一能看到的缺点是生成的MenuItems如何排序,但您可以通过将之前的MenuItems视为一个集合,之后的MenuItems视为另一个集合来解决这个问题。 - micahtan
Micahtan - 也许你应该在你的帖子中使用CompositeCollection类编写解决方案?因为这似乎是真正正确的方法。谢谢,我一直有同样的问题。 - arconaut
arconaut - 尽情享受吧。但在投入生产之前,请先清理一下 :) - micahtan
谢谢 :) 但我并不是真的为自己要代码,只是想把这个变成一个好的问答主题,并且刺激Adrian标记你的帖子为答案。他似乎并不在意或者答案对他来说不合适,虽然 :) - arconaut

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接