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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章