You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF MVVM中MenuItem子菜单打开时的事件处理与动态填充

WPF MVVM 子菜单打开时动态填充内容解决方案

问题场景

我在WPF MVVM应用中,希望在MenuItem的子菜单打开时再填充该子菜单内容。比如名为“Logs”的MenuItem,只有当用户展开它的子菜单时,才去磁盘搜索日志文件,然后把文件名显示在子菜单里。

现有代码结构如下:

MainWindow.xaml

<Grid>
    <Menu ItemsSource="{Binding MenuItems}">
        <Menu.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:MyMenuItemViewModel}"
                                      ItemsSource="{Binding Children}">
                <local:CustomMenuItemControl/>
            </HierarchicalDataTemplate>
        </Menu.Resources>
    </Menu>
</Grid>

MyMenuItemViewModel.cs

public class MyMenuItemViewModel : ObservableObject
{
    public string Text { get; set; }
    public ObservableCollection<MyMenuItemViewModel> Children { get; set; }

    public MyMenuItemViewModel(string item)
    {
        Text = item;
        Children = new ObservableCollection<MyMenuItemViewModel>();
    }
}

目前无法捕获子菜单打开的触发事件,尝试过在HierarchicalDataTemplate和自定义控件中添加处理程序,但未成功,需要找到合适的位置定义触发器或处理程序来执行加载逻辑。


解决方案

方法1:使用Blend行为绑定SubmenuOpened命令(MVVM友好)

这种方式无需修改ViewModel继承关系,也不用写后台代码,完全符合MVVM分离原则。

  1. 安装NuGet包:添加Microsoft.Xaml.Behaviors.Wpf包
  2. 更新ViewModel:添加处理子菜单打开的命令和逻辑
public class MyMenuItemViewModel : ObservableObject
{
    public string Text { get; set; }
    public ObservableCollection<MyMenuItemViewModel> Children { get; set; }
    public ICommand SubmenuOpenedCommand { get; }

    public MyMenuItemViewModel(string item)
    {
        Text = item;
        Children = new ObservableCollection<MyMenuItemViewModel>();
        SubmenuOpenedCommand = new RelayCommand(LoadSubmenuItems);
    }

    private void LoadSubmenuItems()
    {
        // 避免重复加载,可根据需求调整
        if (Children.Count > 0) return;
        
        // 这里替换为实际的日志文件搜索逻辑
        var logFiles = Directory.GetFiles(@"C:\Logs", "*.log");
        foreach (var file in logFiles)
        {
            Children.Add(new MyMenuItemViewModel(Path.GetFileName(file)));
        }
    }
}

注:RelayCommand是通用的ICommand实现,可自行编写或使用Prism/MvvmLight等框架的实现。

  1. 修改XAML模板:给MenuItem添加行为绑定事件
    首先在XAML头部添加行为命名空间:
xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"

然后更新HierarchicalDataTemplate:

<Menu ItemsSource="{Binding MenuItems}">
    <Menu.Resources>
        <HierarchicalDataTemplate DataType="{x:Type local:MyMenuItemViewModel}"
                                  ItemsSource="{Binding Children}">
            <local:CustomMenuItemControl/>
            <!-- 为MenuItem容器绑定SubmenuOpened事件到ViewModel命令 -->
            <HierarchicalDataTemplate.ItemContainerStyle>
                <Style TargetType="MenuItem">
                    <Setter Property="behaviors:Interaction.Triggers">
                        <Setter.Value>
                            <behaviors:EventTrigger EventName="SubmenuOpened">
                                <behaviors:InvokeCommandAction Command="{Binding SubmenuOpenedCommand}"/>
                            </behaviors:EventTrigger>
                        </Setter.Value>
                    </Setter>
                </Style>
            </HierarchicalDataTemplate.ItemContainerStyle>
        </HierarchicalDataTemplate>
    </Menu.Resources>
</Menu>

方法2:使用自定义附加属性(无需NuGet包)

如果不想依赖Blend行为,可以通过自定义附加属性捕获SubmenuOpened事件。

  1. 创建附加属性类:
public static class MenuItemExtensions
{
    public static readonly DependencyProperty SubmenuOpenedCommandProperty =
        DependencyProperty.RegisterAttached(
            "SubmenuOpenedCommand",
            typeof(ICommand),
            typeof(MenuItemExtensions),
            new PropertyMetadata(null, OnSubmenuOpenedCommandChanged));

    public static void SetSubmenuOpenedCommand(MenuItem element, ICommand value)
    {
        element.SetValue(SubmenuOpenedCommandProperty, value);
    }

    public static ICommand GetSubmenuOpenedCommand(MenuItem element)
    {
        return (ICommand)element.GetValue(SubmenuOpenedCommandProperty);
    }

    private static void OnSubmenuOpenedCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MenuItem menuItem)
        {
            menuItem.SubmenuOpened -= MenuItem_SubmenuOpened;
            if (e.NewValue is ICommand command)
            {
                menuItem.SubmenuOpened += MenuItem_SubmenuOpened;
            }
        }
    }

    private static void MenuItem_SubmenuOpened(object sender, RoutedEventArgs e)
    {
        if (sender is MenuItem menuItem)
        {
            var command = GetSubmenuOpenedCommand(menuItem);
            command?.Execute(null);
        }
    }
}
  1. ViewModel修改:和方法1一致,添加SubmenuOpenedCommand和LoadSubmenuItems方法
  2. 更新XAML模板:
<HierarchicalDataTemplate DataType="{x:Type local:MyMenuItemViewModel}"
                          ItemsSource="{Binding Children}">
    <local:CustomMenuItemControl/>
    <HierarchicalDataTemplate.ItemContainerStyle>
        <Style TargetType="MenuItem">
            <Setter Property="local:MenuItemExtensions.SubmenuOpenedCommand"
                    Value="{Binding SubmenuOpenedCommand}"/>
        </Style>
    </HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>

关键注意事项

  • 两种方法均遵循MVVM原则,将视图事件与ViewModel逻辑分离
  • 加载逻辑中加入了重复加载判断,可根据实际需求调整(比如需要刷新内容时移除该判断)
  • 若仅特定MenuItem需要动态加载(如"Logs"),可在LoadSubmenuItems中通过Text属性判断,或给ViewModel添加IsDynamicLoad标记属性

内容的提问来源于stack exchange,提问作者HeadlessJ

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 13:35:13