如何动态地将MenuItems(带标题)添加到WPF菜单

时间:2010-09-16 03:52:48

标签: wpf prism

[编辑#3] - 对于阅读此问题的任何人:在任何情况下都要使用此问题中列出的方法。 这是一个编码恐怖。我很自然地承认这一点,因为我知道所有的程序员都曾在过去的一个角落里工作过(尤其是在学习新技术的时候),我们都被其他善意的开发人员误入歧途。首先阅读罗伯特的答案,然后阅读这个问题。请。

[编辑#2b]

我为这个问题的长度道歉 - 这里有一个问题(最后!),但我想确保源代码是明确的。反正。

[编辑#2] - 问题标题已更改为更准确地反映...问题。

[编辑] - 我已经更新了一些历史记录,了解我最终得到了我在此处所做的设计/代码:Obligatory Blog Post。如果它有助于澄清下面的问题,请随时阅读...

原始问题

我正在使用的应用程序使用Prism和WPF,其中包含许多模块(目前为3个),其中一个模块承载应用程序菜单。最初,菜单是静态的,并且挂钩到CompositeCommand / DelegateCommands,这非常适合将按钮按下到适当的演示者。每个MenuItem在其标题中使用StackPanel将内容显示为图像和文本标签的组合 - 这就是我想要的样子:

<Menu Height="48" Margin="5,0,5,0" Name="MainMenu" VerticalAlignment="Top" Background="Transparent">
                <MenuItem Name="MenuFile" AutomationProperties.AutomationId="File">
                    <MenuItem.Header>
                        <StackPanel>
                            <Image Height="24" VerticalAlignment="Center" Source="../Resources/066.png"/>
                            <ContentPresenter Content="Main"/>
                        </StackPanel>
                    </MenuItem.Header>
                    <MenuItem AutomationProperties.AutomationId="FileExit" Command="{x:Static local:ToolBarCommands.FileExit}">
                        <MenuItem.Header>
                            <StackPanel>
                                <Image Height="24" VerticalAlignment="Center" Source="../Resources/002.png"/>
                                <ContentPresenter Content="Exit"/>
                            </StackPanel>
                        </MenuItem.Header>
                    </MenuItem>
                </MenuItem>
                <MenuItem  Name="MenuHelp" AutomationProperties.AutomationId="Help" Command="{x:Static local:ToolBarCommands.Help}">
                    <MenuItem.Header>
                        <StackPanel>
                            <Image Height="24" VerticalAlignment="Center" Source="../Resources/152.png"/>
                            <ContentPresenter Content="Help"/>
                        </StackPanel>
                    </MenuItem.Header>
                </MenuItem>               
            </Menu>

不幸的是,应用程序变得有点复杂,并且希望让其他模块在菜单中注册自己 - 因此,我一直在考虑使菜单动态化。目标是让其他模块(通过服务)能够随意向菜单添加命令 - 例如,模块A将在工具栏模块中添加一个菜单项,该模块调用模块A中的处理程序。有一些优秀的文章关于这个问题 - 我看过的两个是Building a Databound WPF Menu Using a HierarchicalDataTemplateWPF Sample Series - Databound HierarchicalDataTemplate Menu Sample。按照本文中的建议,我设法创建一个动态构造的菜单,没有明显的数据绑定问题 - 它可以创建一个菜单,其中的项目链接到我的表示模型,反映了演示模型中ObservableCollection的结构

目前,我的XAML如下所示:

<UserControl x:Class="Modules.ToolBar.Views.ToolBarView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:model="clr-namespace:Modules.ToolBar.PresentationModels"
    xmlns:local="clr-namespace:Modules.ToolBar">
    <UserControl.Resources>
        <model:ToolBarPresentationModel x:Key="modelData" />
        <HierarchicalDataTemplate DataType="{x:Type model:ToolbarObject}"
                                  ItemsSource="{Binding Path=Children}">
            <ContentPresenter Content="{Binding Path=Name}"
                              Loaded="ContentPresenter_Loaded"
                              RecognizesAccessKey="True"/>
        </HierarchicalDataTemplate>
    </UserControl.Resources>
    <UserControl.DataContext>
        <Binding Source="{StaticResource modelData}"/>
    </UserControl.DataContext>
        <Menu Height="48" Margin="5,0,5,0" Name="MainMenu" VerticalAlignment="Top" Background="Transparent"
            ItemsSource="{Binding}">
        </Menu>
    </Grid>
</UserControl>

视图背后的代码完成了ContentPresenter_Loaded方法中的繁重工作:

   private void ContentPresenter_Loaded(object sender, System.Windows.RoutedEventArgs e)
    {
    ContentPresenter presenter = sender as ContentPresenter;
    if (sender != null)
    {
        DependencyObject parentObject = VisualTreeHelper.GetParent(presenter);
        bool bContinue = true;
        while (bContinue
            || parentObject == null)
        {
            if (parentObject is MenuItem)
                bContinue = false;
            else
                parentObject = VisualTreeHelper.GetParent(parentObject);
        }
        var menuItem = parentObject as MenuItem;
        if (menuItem != null)
        {
            ToolbarObject toolbarObject = menuItem.DataContext as ToolbarObject;
            StackPanel panel = new StackPanel();
            if (!String.IsNullOrEmpty(toolbarObject.ImageLocation))
            {
                Image image = new Image();
                image.Height = 24;
                image.VerticalAlignment = System.Windows.VerticalAlignment.Center;
                Binding sourceBinding = new Binding("ImageLocation");
                sourceBinding.Mode = BindingMode.TwoWay;
                sourceBinding.Source = toolbarObject;
                image.SetBinding(Image.SourceProperty, sourceBinding);
                panel.Children.Add(image);
            }
            ContentPresenter contentPresenter = new ContentPresenter();
            Binding contentBinding = new Binding("Name");
            contentBinding.Mode = BindingMode.TwoWay;
            contentBinding.Source = toolbarObject;
            contentPresenter.SetBinding(ContentPresenter.ContentProperty, contentBinding);
            panel.Children.Add(contentPresenter);
            menuItem.Header = panel;
            Binding commandBinding = new Binding("Command");
            commandBinding.Mode = BindingMode.TwoWay;
            commandBinding.Source = toolbarObject;
            menuItem.SetBinding(MenuItem.CommandProperty, commandBinding);                    
        }
    }
}

正如您所看到的,我正在尝试重新创建原始菜单的StackPanel / Image / Name组合,只是在后面的代码中这样做。试图做到这一点并没有那么好 - 虽然菜单对象肯定是在创建的,但它们并不像“空白”,可点击的对象那样“出现” - StackPanel,Image,Name等都没有被渲染。有趣的是,它还会导致HierarchicalDataTemplate中ContentPresent中的原始文本被删除。

那么问题是,有没有办法在Load事件中设置MenuItem的Header属性,以便它在UserControl上正确显示?是否显示标题中的项目表示DataBinding问题?如果是这样,将Header绑定到瞬态对象(在load事件处理程序中创建的StackPanel)的正确方法是什么?

我愿意改变上面代码中的任何内容 - 这就是各种原型,试图找出处理动态菜单创建的最佳方法。 谢谢!

2 个答案:

答案 0 :(得分:9)

我承认我没有像我应该那样深入挖掘你的例子,但是每当我看到搜索可视树的代码隐藏时,我认为,这可以在视图模型中更明确地处理?

在这种情况下,在我看来,您可以想出一个非常简单的视图模型 - 一个公开TextImageCommandChildren属性的对象例如 - 然后创建一个简单的数据模板,用于将其呈现为MenuItem。然后,任何需要改变菜单内容的东西都会操纵这个模型。

修改

仔细查看了您的内容,以及您在博文中链接的两个示例,我正在敲打桌子。这两个开发人员似乎都误以为在模板生成的菜单项上设置属性的方法是在创建ContentPresenter.Load事件后搜索可视树。不是这样。这就是ItemContainerStyle的用途。

如果您使用它,那么创建您所描述类型的动态菜单非常简单。您需要MenuItemViewModel类已实现INotifyPropertyChanged并公开这些公共属性:

string Text
Uri ImageSource
ICommand Command
ObservableCollection<MenuItemViewModel> Children

使用此:

<Menu DockPanel.Dock="Top" ItemsSource="{DynamicResource Menu}"/>

其中ItemsSourceObservableCollection<MenuItemViewModel>,并使用此模板:

<HierarchicalDataTemplate DataType="{x:Type local:MenuItemViewModel}"
                          ItemsSource="{Binding Path=Children}">
    <HierarchicalDataTemplate.ItemContainerStyle>
        <Style TargetType="MenuItem">
            <Setter Property="Command"
                    Value="{Binding Command}" />
        </Style>
    </HierarchicalDataTemplate.ItemContainerStyle>
    <StackPanel Orientation="Horizontal">
        <Image Source="{Binding ImageSource}" />
        <Label Content="{Binding Text}" />
    </StackPanel>
</HierarchicalDataTemplate>

窗口中的菜单准确表示集合中的内容,并在项目添加和删除时动态更新,包括顶级项目和后代。

在可视化树中没有任何攀爬,没有手动创建对象,没有代码隐藏(除了在视图模型中,以及首先填充集合的任何内容)。

我已经建立了一个非常完善的例子;您可以下载项目here

答案 1 :(得分:0)

另一种可能的方法是让Menu成为一个区域并同意约定,以便添加到该区域的所有视图都有一个ViewModel,其属性名为MenuHeader。这样,区域适配器可以简单地从View的数据上下文中获取菜单标题,并在添加时将其设置为项目。

在Prism中做了类似的事情,并将视图添加到选项卡区域。您可以阅读更多here

我希望这能提供一些有用的指导。

谢谢, 达米安