Skip to content

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

Published: at 15:27

原始需求

一个菜单项(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://blog.jgrass.cc"/>
</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 博客


原文链接: https://blog.jgrass.cc/posts/wpf-markup-extension-property/

本作品采用 「署名 4.0 国际」 许可协议进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。