如何在WPF菜单中动态添加带标题的菜单项

10

[第三次编辑] - 对于阅读此问题的任何人:千万不要使用本问题中概述的方法。这是一场编程恐慌。我自由地承认这一点,因为所有程序员在过去都曾陷入困境,而且(特别是在学习新技术时)我们都曾被网络上其他好心的开发人员误导。请先阅读Robert的答案,然后再阅读这个问题。请。

[第二次编辑b]

对于问题长度我深感歉意-最后真正的问题藏在问题之中,但我希望确保源代码是明确的。无论如何。

[第二次编辑] - 将问题标题更改为更准确地反映问题。

[编辑] - 我在这里更新了更多关于我如何设计/编写代码的历史记录:Obligatory Blog Post。如果它有助于澄清下面的问题,请随时阅读它...

原始问题

我正在处理的应用程序使用Prism和WPF,其中包含多个模块(目前为3个),其中一个模块托管应用程序菜单。最初,该菜单是静态的,并使用CompositeCommand / DelegateCommands中的钩子,这非常适用于将按钮按下路由到适当的Presenter。每个菜单项在其标题中使用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中的处理程序。有几篇关于这个主题的优秀文章-我看过的两篇文章是使用分层数据模板构建数据绑定的WPF菜单WPF样本系列 - 数据绑定分层数据模板菜单样例。按照这篇文章的建议,我成功地创建了一个动态构建的菜单,没有明显的数据绑定问题-它可以创建一个与我的呈现模型中的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的ContentPresenter中的原始文本被擦除。

那么问题来了,有没有一种方法可以在Load事件中设置MenuItem的Header属性,使其能够正确地显示在UserControl上? Header中的项目未显示是否表明存在数据绑定问题?如果是,将Header绑定到暂时对象(在load事件处理程序中创建的StackPanel)的正确方法是什么?

我愿意更改以上代码的任何内容,这些都是试验性的,试图找到处理动态菜单创建的最佳方法。 谢谢!


1
+1 对第三次编辑,节省了我大量阅读的时间 :) - dain
2个回答

10
我承认我还没有深入研究你的示例,但每当我看到代码后面在搜索可视树时,我就会想,这是否可以更明确地在视图模型中处理?
在这种情况下,似乎你可以设计一个相当简单的视图模型,例如,公开“Text”、“Image”、“Command”和“Children”属性的对象,然后创建一个简单的数据模板来将其呈现为一个“MenuItem”。然后需要更改菜单内容的任何内容都可以操作此模型。
编辑:
经过仔细查看你的情况以及你在博客文章中链接的两个示例,我感到非常头疼。这两个开发人员似乎误解了设置模板生成的菜单项上的属性的方法是在ContentPresenter.Load事件后通过搜索可视树来实现。事实并非如此。这正是ItemContainerStyle的用途。
如果使用它,就可以轻松创建所描述的动态菜单。你需要一个“MenuItemViewModel”类,其中实现了INotifyPropertyChanged并公开了以下公共属性:
string Text
Uri ImageSource
ICommand Command
ObservableCollection<MenuItemViewModel> Children

使用这个:

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

假设 ItemsSource 是一个 ObservableCollection<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>

窗口中的菜单完全代表了集合中的内容,并随着项目的添加和删除而动态更新,无论是顶级项目还是子项。

在可视化树中没有爬行,也没有手动创建对象,没有代码后台(除了在视图模型中以及首次填充集合的任何位置)。

我已经建立了一个非常完整的示例;您可以在这里下载该项目。


这里有一个 MenuItem 的视图模型 - 在这种情况下,它是 ToolbarObject。它公开了菜单名称、文本、图像资源标识符的 ICommand 对象,以及子项(这是一个ToolbarObjects的可观察集合)。我不希望图像成为其中的一部分 - 这似乎会让视图模型变得混乱。HierarchicalDataTemplate 已经从该对象中触发。遍历树对于获取嵌套菜单中命令对象的绑定是必要的 - 使用简单的 DataTemplate 是无法实现的。 - Matt Jordan
好的,然而我遇到了HierarchicalDataTemplate的问题,菜单的子项没有被渲染或更新。由于这些子项在ObservableCollection中,我不确定发生了什么...这也是我寻找另一种方法的唯一原因。你有任何想法为什么它们没有被捕捉到吗? - Matt Jordan
1
很难说;这条路上已经涂了很多气味。在你发布的例子中,我知道你是在使用HierarchicalDataTemplate显式地创建MenuItem对象。如果你正在遍历可视树,这将导致混乱,因为一个MenuItem的父级MenuItem实际上不是父菜单,而是项目容器。我真心建议您查看我编写的演示应用程序,看看是否可以将其适应于您的目的,或者您是否可以在其中复现您所描述的问题。 - Robert Rossney
@RobertRossney,演示现在无法下载。 - Steve.NayLinAung
哇,6年过去了。你的演示不再可下载。 - Jack
显示剩余5条评论

0

另一种可能的方法是将菜单作为一个区域,并达成共识,使得添加到该区域的所有视图都具有一个名为MenuHeader的属性的ViewModel。这样,区域适配器可以简单地从视图的数据上下文中获取菜单标题,并在添加时将其设置为该项。

类似的事情在Prism中对添加到标签区域的视图也是如此。您可以在这里阅读更多信息。

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

谢谢, Damian


感谢提供另一种方法 - 我曾看到过使用选项卡页来实现,但没有尝试过使用菜单。我认为Robert所概述的方法在某种意义上更加“简洁”,因为它不会通过区域来复杂化方法 - 但绝对值得研究(在某些情况下,这种复杂性可能是值得的)。 - Matt Jordan
当然,我理解创建区域所带来的“额外复杂性”。虽然这可能不是特定的情况,但使用Prism的可扩展性来为该控件创建区域、区域适配器等应该是有用的,而您只需要将菜单项作为简单视图添加到区域中即可。 - Damian Schenkelman

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