如何将 MenuItems(带有标题)动态添加到 WPF 菜单 [英] How to dynamically add MenuItems (with a header) to a WPF menu

查看:55
本文介绍了如何将 MenuItems(带有标题)动态添加到 WPF 菜单的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

[Edit #3] - 对于阅读此问题的任何人:在任何情况下都不要使用此问题中概述的方法.这是一种编码恐怖.我坦率地承认这一点,因为我知道过去所有程序员都把自己逼到了一个角落,而且(尤其是在学习新技术时)我们都被互联网上其他善意的开发人员引入了歧途.首先阅读罗伯特的答案,然后阅读这个问题.请.

- to anyone reading this question: do not under any circumstance use the approach outlined in this question. It is a Coding Horror. I freely admit this, knowing that all programmers have worked themselves into a corner in the past, and (especially when learning a new technology) we all have been led astray by other, well-meaning developers on the interweb. Read the answer by Robert first, then read this question. Please.

我为这个问题的长度道歉 - 这里有一个问题(在最后!),但我想确保源代码是明确的.无论如何.

I apologize for the length of this question - there is a question in here (at the end!), but I wanted to make sure the source code was explicit. Anyway.

[Edit #2] - 更改了问题标题以更准确地反映...问题.

- question title changed to more accurately reflect the... question.

- 我已经更新了更多关于我如何完成设计/代码的历史记录:强制性博客帖子.如果它有助于澄清下面的问题,请随时阅读...

- I've updated some more of the history as to how I ended up at the design / code that I did here: Obligatory Blog Post. If it helps clarify the question below, feel free to read it...

原始问题

我正在开发的应用程序使用 Prism 和 WPF,具有多个模块(目前为 3 个),其中一个模块承载应用程序菜单.最初,菜单是静态的,带有连接到 CompositeCommand/DelegateCommands 的挂钩,这对于将按钮按下路由到适当的演示者非常有用.每个 MenuItem 在其标题中使用 StackPanel 将内容显示为图像和文本标签的组合 - 这就是我想要的外观:

The application I'm working on uses Prism and WPF, with a number of modules (currently 3), one of which hosts the application menu. Originally, the menu was static with hooks into CompositeCommand / DelegateCommands, which worked great for routing button presses to the appropriate presenter. Each MenuItem used a StackPanel in its header to display the content as a combination of an image and a text label - which was the look I was going for:

<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 中的处理程序.有一些优秀的文章关于这个主题 - 我看过的两个是 使用 HierarchicalDataTemplateWPF 示例系列 - 数据绑定 HierarchicalDataTemplate 菜单示例.按照文章中的建议,我设法制作了一个动态构造的菜单,没有明显的数据绑定问题——它可以创建一个菜单,其中的项目链接到我的演示模型,反映了演示模型中 ObservableCollection 的结构

Unfortunately, the application has gotten a bit more complex and it is desireable to have other modules register themselves with the menu - hence, I've been looking at making the menu dynamic. The goal is to have other modules (through a service) be able to add commands to the menu at will - for example, Module A will add a menu item in the Toolbar module that calls a handler in Module A. There's a few excellent articles out there on this subject - the two I've looked at are Building a Databound WPF Menu Using a HierarchicalDataTemplate and WPF Sample Series - Databound HierarchicalDataTemplate Menu Sample. Following the advice in the article, I have managed to make a dynamically constructed menu with no obvious data binding problems - it can create a menu with items linked backed to my presentation model, reflecting the structure of an ObservableCollection in the presentation model

目前,我的 XAML 如下所示:

Currently, my XAML looks like the following:

<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 方法中完成了繁重的工作:

The code behind for the view does the heavy lifting in the ContentPresenter_Loaded method:

   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,图像,名称等没有被呈现.有趣的是,它还导致 HierarchicalDataTemplate 中 ContentPresent 中的原始文本被擦除.

As you can see, I'm attempting to recreate the StackPanel / Image / Name combination of the original menu, just doing so in the code behind. Attempting to do this has not worked out so well - while the menu objects are certainly being created, they don't "appear" as anything other than blank, clickable objects - the StackPanel, Image, Name, etc. aren't being rendered. Interestingly enough, it also is causing the original text in the ContentPresent in the HierarchicalDataTemplate to be erased.

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

The question then, is there a way to set a MenuItem's Header property in the Load event such that it will display on the UserControl properly? Is the fact that the items in the header are not being displayed indicative of a DataBinding problem? If so, what would be the proper way to bind the Header to a transient object (the StackPanel that was created in the load event handler)?

我愿意更改上面代码中的任何内容 - 这是所有类型的原型设计,试图找出处理动态菜单创建的最佳方式.谢谢!

I'm open to changing anything in the code above - this is all sort of prototyping along, trying to figure out the best way to handle dynamic menu creation. Thanks!

推荐答案

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

I'll confess that I haven't dug quite as deep into your example as maybe I should, but whenever I see code-behind that's searching the visual tree, I think, could this be handled more explicitly in a view model?

在我看来,在这种情况下,您可以提出一个非常简单的视图模型 - 一个暴露 TextImageCommand 的对象> 和 Children 属性,例如 - 然后创建一个简单的数据模板,将其显示为 MenuItem.然后,任何需要更改菜单内容的操作都会操纵此模型.

It seems to me in this case that you could come up with a pretty straightforward view model - an object exposing Text, Image, Command, and Children properties, for instance - and then create a simple data template that for presenting it as a MenuItem. Then anything that needs to alter the contents of your menus manipulates this model.

更详细地了解了您的工作以及您在博客文章中链接到的两个示例后,我正用头撞在桌子上.这两个开发人员似乎都误解了在模板生成的菜单项上设置属性的方法是在它们之后搜索 ContentPresenter.Load 事件中的可视化树重新创建.不是这样.这就是 ItemContainerStyle 的用途.

Having looked at what you're up to in more detail, and the two examples you've linked to in your blog post, I am banging my head against the desk. Both of those developers appear to be under the misapprehension that the way to set properties on the menu items that are being generated by the template is to search through the visual tree in the ContentPresenter.Load event after they're created. Not so. That's is what the ItemContainerStyle is for.

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

If you use that, it's quite straightforward to create dynamic menus of the type you're describing. You need a MenuItemViewModel class that has INotifyPropertyChanged implemented and exposes these public properties:

string Text
Uri ImageSource
ICommand Command
ObservableCollection<MenuItemViewModel> Children

使用:

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

其中 ItemsSource 是一个 ObservableCollection,并且使用这个模板:

where the ItemsSource is an ObservableCollection<MenuItemViewModel>, and using this template:

<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>

窗口中的菜单完全代表集合中的内容,并且随着项目的添加和删除而动态更新,包括顶级项目和后代.

the menus in the window exactly represent what's in the collection, and are dynamically updated as items are added and removed, both to the top-level items and to the descendants.

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

There's no clambering about in the visual tree, no manual creation of objects, no code-behind (other than in the view model, and in whatever populates the collection in the first place).

我已经为此构建了一个非常彻底的示例;您可以在此处下载该项目.

I've built a pretty thoroughly worked example of this; you can download the project here.

这篇关于如何将 MenuItems(带有标题)动态添加到 WPF 菜单的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆