原始需求
一個菜單項(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 博客