原始需求
一个菜单项(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 国际」 许可协议进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。