WPF 使用 MarkupExtension 实现更灵活的属性赋值与控制

原始需求#

一个菜单项(MenuItem)有多个子菜单,如果所有子菜单都不可见,则父菜单也隐藏。

一个直接的实现思路是,使用 MultiBinding,将父菜单的 Visibility 属性,绑定到所有子菜单上。但这种写法,在子菜单变更时,需要手动修改代码,而且其它业务也需要这个功能时,难以直接复用。

使用 MarkupExtension 的实现方式#


    /// <summary>
    /// 父菜单是否可见,由全部的子菜单决定;如果所有的子菜单都不可见,则父菜单不可见
    /// </summary>
    internal class ParentMenuItemVisibilityConverter : MarkupExtension
    {
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var service = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            var targetProperty = service?.TargetProperty as DependencyProperty;
            var targetObject = service?.TargetObject;

            if (targetObject is MenuItem menu && targetProperty != null)
            {
                // 在父菜单 Loaded 时,检查所有子菜单的可见性,决定父菜单的可见性
                menu.Loaded += (sender, args) =>
                {
                    menu.Visibility = CheckParentVisibility(menu);
                };
                return Visibility.Visible;

            }
            else
            {
                return DependencyProperty.UnsetValue;
            }
        }

        private Visibility CheckParentVisibility(MenuItem? parentMenu)
        {
            if (parentMenu is { } menu)
            {
                var menuItems = menu.Items;
                foreach (var itemItem in menuItems)
                {
                    if (itemItem is MenuItem item)
                    {
                        if (item.Visibility == Visibility.Visible)
                        {
                            // 只要有一个子菜单可见,则父菜单项课件
                            return Visibility.Visible;
                        }
                    }
                }
            }

            return Visibility.Collapsed;
        }

使用:

<MenuItem Header="帮助"
          x:Name="HelpMenuItem"
          Visibility="{local:ParentMenuItemVisibilityConverter}">

    <MenuItem Header="帮助1">
    </MenuItem>
    <MenuItem Header="帮助2">
    </MenuItem>
    <MenuItem Header="https://www.cnblogs.com/jasongrass/"/>

</MenuItem>

简单来说就是,在 MarkupExtension 的实现中,可以拿到 父菜单 的实例,可以订阅其 Loaded 事件,在这里更新 Visibility 属性。

重点说明#

使用 MarkupExtension 的好处时,里面可以拿到操作的实例,属性等上下文信息,而如果只是写普通的 Converter,有些数据拿不到,使用 MarkupExtension 更灵活。
但另一方面,需要根据自己的业务逻辑,确定具体的实现方式,上面使用 Loaded 事件可以处理,但在有些业务场景下,就不一定适用了。

其它玩法#

在 MarkupExtension.ProvideValue 中,除了返回属性对应的值,还可以返回 Binding,相当于在 XAML 中直接写 Binding,但好处是,这里可以拿到更多的上下文信息,Binging 可以非常灵活的执行。

下面这里例子,就是一个更复杂的写法(实际中没有必要)。
这里返回了一个 Binding,而此 Binding 有一个 Converter,这个 Converter,就可以拿到很多直接在 XAML 写拿不到的数据(比如父菜单本身,直接在 XAML 拿会造成循环引用)。


    internal class ParentMenuItemVisibilityConverter : MarkupExtension, IValueConverter
    {

        public MenuItem? MenuItem { get; set; }

        public Binding? Binding { get; set; }


        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return CheckParentVisibility(MenuItem);
        }

        private void ItemOnLoaded(object sender, RoutedEventArgs e)
        {
            // 手动通过绑定更新值
            MenuItem?.GetBindingExpression(UIElement.VisibilityProperty)?.UpdateTarget();
        }

        private void ItemOnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            // 手动通过绑定更新值
            MenuItem?.GetBindingExpression(UIElement.VisibilityProperty)?.UpdateTarget();
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var service = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            var targetProperty = service?.TargetProperty as DependencyProperty;
            var targetObject = service?.TargetObject;

            if (targetObject is MenuItem menu && targetProperty != null)
            {

                var binding = new Binding
                {
                    Source = menu,
                    Path = new PropertyPath("Items.Count"),
                    Converter = this,
                };

                this.MenuItem = menu;
                this.Binding = binding;

                BindingOperations.SetBinding(menu, targetProperty, binding);
                return binding.ProvideValue(serviceProvider); // 返回一个 Binding 

                ////menu.Loaded += (sender, args) =>
                ////{
                ////    menu.Visibility = CheckParentVisibility(menu);
                ////};

                ////return Visibility.Visible;

            }
            else
            {
                throw new InvalidOperationException("ParentMenuItemVisibilityConverter 只能用于 MenuItem 的 Visibility 属性");
            }
        }

        private Visibility CheckParentVisibility(MenuItem? menu1)
        {
            if (menu1 is { } menu)
            {
                var menuItems = menu.Items;
                foreach (var itemItem in menuItems)
                {
                    if (itemItem is MenuItem item)
                    {
                        item.IsVisibleChanged -= ItemOnIsVisibleChanged;
                        item.IsVisibleChanged += ItemOnIsVisibleChanged;
                        item.Loaded -= ItemOnLoaded;
                        item.Loaded += ItemOnLoaded;

                        if (item.Visibility == Visibility.Visible)
                        {
                            return Visibility.Visible;
                        }
                    }
                }
            }

            return Visibility.Collapsed;
        }
    }

总结#

MarkupExtension 用来可以比较灵活,毕竟 Binding 的基类就是 MarkupExtension,灵活也会带来问题,处理不好可能会引入内存泄漏(事件订阅那里),重复执行等问题。

参考文章#

Markup Extensions and XAML - WPF .NET Framework | Microsoft Learn
WPF 中自定义 MarkupExtension - Hello—— 寻梦者! - 博客园
如何编写 WPF 的标记扩展 MarkupExtension,即便在 ControlTemplate/DataTemplate 中也能生效_walter lv 的博客 - CSDN 博客

作者:JasonGrass

出处:https://www.cnblogs.com/jasongrass/p/16788318.html

版权:本作品采用「署名 4.0 国际」许可协议进行许可。

posted @   J.晒太阳的猫  阅读(1308)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示