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 博客
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具