WPF 的 ElementName 在 ContextMenu 中无法绑定成功?试试使用 x:Reference!

简介: 原文:WPF 的 ElementName 在 ContextMenu 中无法绑定成功?试试使用 x:Reference! 版权声明:本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
原文: WPF 的 ElementName 在 ContextMenu 中无法绑定成功?试试使用 x:Reference!

版权声明:本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:http://blog.csdn.net/wpwalter/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系(walter.lv@qq.com)。 https://blog.csdn.net/WPwalter/article/details/83473313

在 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 绑定
▲ 使用普通的 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>

注意,MenuItemHeader 属性设置为和 RunText 属性一模一样的绑定字符串。不过运行之后的截图显示,右键菜单中并没有如预期般出现绑定的字符串。

在 ContextMenu 中使用了 ElementName 绑定

使用 x:Reference 代替 ElementName 能够解决

以上绑定失败的原因,是 Grid.ContextMenu 属性中赋值的 ContextMenu 不在可视化树中,而 ContextMenu 又不是一个默认建立 ScopeName 的控件,此时既没有自己指定 NameScope,有没有通过可视化树寻找上层设置的 NameScope,所以在绑定上下文中是找不到 WalterlvWindow 的。如果调用去查找,得到的是 null。详见:WPF 中的 NameScope

类似的情况也发生在设置非可视化树或逻辑树的属性时,典型的比如在 Grid.RowGrid.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’.

因为给 MenuItemHeader 属性绑定赋值的时候,创建绑定表达式用到了 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}";
}

现在运行,右键菜单已经正常完成了绑定。

右键菜单完成了绑定
▲ 右键菜单已经正常完成了绑定


参考资料

目录
相关文章
WPF—多重绑定和跨层级绑定
WPF—多重绑定和跨层级绑定
|
C# 数据格式 XML
WPF 资源(StaticResource 静态资源、DynamicResource 动态资源、添加二进制资源、绑定资源树)
原文:WPF 资源(StaticResource 静态资源、DynamicResource 动态资源、添加二进制资源、绑定资源树) 一、WPF对象级(Window对象)资源的定义与查找 实例一: StaticR...
8376 0
|
C# 数据库
WPF中DataGrid控件绑定数据源
WPF中DataGrid控件绑定数据源
160 0
|
C#
WPF更新绑定字段
WPF更新绑定字段
97 0
|
前端开发 C#
WPF 之 数据与命令绑定 (MVVM方式)
WPF 之 数据与命令绑定 (MVVM方式)
192 0
WPF 之 数据与命令绑定 (MVVM方式)
|
C# 前端开发
wpf中的datagrid绑定操作按钮是否显示或者隐藏
如图,需要在wpf中的datagrid的操作那列有个确认按钮,然后在某些条件下确认按钮可见,某些情况下不可见的,放在mvc里直接在cshtml页面中if..else就行了。 但是在wpf里不行。。网上搜索了好久才找到解决方法,原来只是binding那个visiable属性就行了,
6890 0
|
C# Windows
WPF Toolkit AutoCompleteBox 实体类绑定 关键字自定义关联搜索匹配
原文:WPF Toolkit AutoCompleteBox 实体类绑定 关键字自定义关联搜索匹配 WPF Toolkit AutoCompleteBox 实体类绑定 关键字自定义关联搜索匹配 网上的例子都是零散的   翻阅了 很多篇文章后 再根据 自己项目的实际需求  整理出一个完整的 应用例子...
1081 0
|
C#
WPF使用HierarchicalDataTemplate绑定Dictionary生成TreeView
原文:WPF使用HierarchicalDataTemplate绑定Dictionary生成TreeViewDictionary中的CustomeType是一个集合,将其绑定生成一棵树,树的第一层节点是Dictionary的Key,第二层是CustomeType集合,所有代码用XAML实现。
1526 0
|
C# 数据安全/隐私保护 前端开发
[WPF]实现密码框的密码绑定
原文:[WPF]实现密码框的密码绑定                                                 [WPF]实现密码框的密码绑定                                                            周银辉 正如...
1002 0
|
C#
WPF——TargetNullValue(如何在绑定空值显示默认字符)
原文:WPF——TargetNullValue(如何在绑定空值显示默认字符) 说明:在数据绑定时,如果有些字段为空值,那么在数据绑定时可以用默认值来显示为空的字段。   1 2 3 4 5 ...
1048 0