【XAML】WPF 的 ElementName 在 ContextMenu 中无法绑定成功?试试使用 x:Reference!
原文:https://blog.walterlv.com/post/fix-wpf-binding-issues-in-context-menu.html
在 Binding 中使用 ElementName 司空见惯,没见它出过什么事儿。不过当你预见 ContextMenu,或者类似 Grid.Row / Grid.Column 这样的属性中设置的时候,ElementName 就不那么管用了。
本文将解决这个问题。
以下代码是可以正常工作的
<Window x:Class="Walterlv.Demo.BindingContext.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="WalterlvWindow" Title="Walterlv Binding Demo" Height="450" Width="800"> <Grid Background="LightGray" Margin="1 1 1 0" MinHeight="40"> <TextBlock> <Run Text="{Binding Mode=OneWay}" FontSize="20" /> <LineBreak /> <Run Text="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> </TextBlock> </Grid> </Window>
在代码中,我们为一段文字中的一个部分绑定了主窗口的的一个属性,于是我们使用 ElementName
来指定绑定源为 WalterlvWindow
。
使用普通的 ElementName 绑定
以下代码就无法正常工作了
保持以上代码不变,我们现在新增一个 ContextMenu
,然后在 ContextMenu
中使用一模一样的绑定表达式:
<Window x:Class="Walterlv.Demo.BindingContext.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="WalterlvWindow" Title="Walterlv Binding Demo" Height="450" Width="800"> <Grid Background="LightGray" Margin="1 1 1 0" MinHeight="40"> <Grid.ContextMenu> <ContextMenu> <MenuItem Header="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> </ContextMenu> </Grid.ContextMenu> <TextBlock> <Run Text="{Binding Mode=OneWay}" FontSize="20" /> <LineBreak /> <Run Text="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> </TextBlock> </Grid> </Window>
注意,MenuItem
的 Header
属性设置为和 Run
的 Text
属性一模一样的绑定字符串。不过运行之后的截图显示,右键菜单中并没有如预期般出现绑定的字符串。
使用 x:Reference 代替 ElementName 能够解决
以上绑定失败的原因,是 Grid.ContextMenu
属性中赋值的 ContextMenu
不在可视化树中,而 ContextMenu
又不是一个默认建立 ScopeName 的控件,此时既没有自己指定 NameScope,有没有通过可视化树寻找上层设置的 NameScope,所以在绑定上下文中是找不到 WalterlvWindow
的。如果调用去查找,得到的是 null
。详见:WPF 中的 NameScope。
类似的情况也发生在设置非可视化树或逻辑树的属性时,典型的比如在 Grid.Row
或 Grid.Column
属性上绑定时,ElementName
也是失效的。
此时最适合的情况是直接使用 x:Reference
。
<Window x:Class="Walterlv.Demo.BindingContext.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="WalterlvWindow" Title="Walterlv Binding Demo" Height="450" Width="800"> <Grid Background="LightGray" Margin="1 1 1 0" MinHeight="40"> <Grid.ContextMenu> <ContextMenu> - <MenuItem Header="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> + <MenuItem Header="{Binding Source={x:Reference WalterlvWindow}, Path=DemoText, Mode=OneWay}" /> </ContextMenu> </Grid.ContextMenu> <TextBlock> <Run Text="{Binding Mode=OneWay}" FontSize="20" /> <LineBreak /> <Run Text="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> </TextBlock> </Grid> </Window>
不过,这是个假象,因为此代码运行时会抛出异常:
XamlObjectWriterException: Cannot call MarkupExtension.ProvideValue because of a cyclical dependency. Properties inside a MarkupExtension cannot reference objects that reference the result of the MarkupExtension. The affected MarkupExtensions are:
‘System.Windows.Data.Binding’ Line number ‘8’ and line position ‘27’.
因为给 MenuItem
的 Header
属性绑定赋值的时候,创建绑定表达式用到了 WalterlvWindow
,但此时 WalterlvWindow
尚在构建(因为里面的 ContextMenu
是窗口的一部分),于是出现了循环依赖。而这是不允许的。
为了解决循环依赖问题,我们可以考虑将 x:Reference
放到资源中。因为资源是按需创建的,所以这不会造成循环依赖。
那么总得有一个对象来承载我们的绑定源。拿控件的 Tag
属性也许是一个方案,不过专门为此建立一个绑定代理类也许是一个更符合语义的方法:
<Window x:Class="Walterlv.Demo.BindingContext.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="clr-namespace:Walterlv.Demo.BindingContext" x:Name="WalterlvWindow" Title="Walterlv Binding Demo" Height="450" Width="800"> + <Window.Resources> + <local:BindingProxy x:Key="WalterlvBindingProxy" Data="{x:Reference WalterlvWindow}" /> + </Window.Resources> <Grid Background="LightGray" Margin="1 1 1 0" MinHeight="40"> <Grid.ContextMenu> <ContextMenu> - <MenuItem Header="{Binding Source={x:Reference WalterlvWindow}, Path=DemoText, Mode=OneWay}" /> + <MenuItem Header="{Binding Source={StaticResource WalterlvBindingProxy}, Path=Data.DemoText, Mode=OneWay}" /> </ContextMenu> </Grid.ContextMenu> <TextBlock> <Run Text="{Binding Mode=OneWay}" FontSize="20" /> <LineBreak /> <Run Text="{Binding ElementName=WalterlvWindow, Path=DemoText, Mode=OneWay}" /> </TextBlock> </Grid> </Window>
至于 BindingProxy
,非常简单:
public sealed class BindingProxy : Freezable { public static readonly DependencyProperty DataProperty = DependencyProperty.Register( "Data", typeof(object), typeof(BindingProxy), new PropertyMetadata(default(object))); public object Data { get => (object) GetValue(DataProperty); set => SetValue(DataProperty, value); } protected override Freezable CreateInstanceCore() => new BindingProxy(); public override string ToString() => Data is FrameworkElement fe ? $"BindingProxy: {fe.Name}" : $"Binding Proxy: {Data?.GetType().FullName}"; }
现在运行,右键菜单已经正常完成了绑定。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
2021-11-09 一句话归纳设计模式
2021-11-09 【C#DLR 动态编程】CallSite<T>
2021-11-09 【C# 基础概念】C# 4 dynamic - var, object, dynamic的区别以及dynamic的使用