当前位置: 首页 > news >正文

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

在 Binding 中使用 ElementName 司空见惯,没见它出过什么事儿。不过当你预见 ContextMenu,或者类似 Grid.Row / Grid.Column 这样的属性中设置的时候,ElementName 就不那么管用了。

本文将解决这个问题。


本文内容

      • 以下代码是可以正常工作的
      • 以下代码就无法正常工作了
      • 使用 x:Reference 代替 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}";
}

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

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


参考资料

  • c# - WPF databinding error in Tag property - Stack Overflow

相关文章:

  • WPF 中的 NameScope
  • Windows 下的高 DPI 应用开发(UWP / WPF / Windows Forms / Win32)
  • 技术、产品、交流、思考 - 微软技术暨生态大会 2018
  • .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
  • .NET/C# 异常处理:写一个空的 try 块代码,而把重要代码写到 finally 中(Constrained Execution Regions)
  • WindowsXamlHost:在 WPF 中使用 UWP 控件库中的控件
  • WindowsXamlHost:在 WPF 中使用 UWP 的控件(Windows Community Toolkit)
  • .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)
  • 编写 Target 检测 MSBuild / dotnet build 此次编译是否是差量编译
  • 使用 Win2D 绘制带图片纹理的圆(或椭圆)
  • Win2D 中的游戏循环:CanvasAnimatedControl
  • 使用 Windows 10 中的加速度计(Accelerometer,重力传感器)
  • 用 dotTrace 进行性能分析时,各种不同性能分析选项的含义和用途
  • 如何创建一个基于 .NET Core 3 的 WPF 项目
  • 将基于 .NET Framework 的 WPF 项目迁移到基于 .NET Core 3
  • 【从零开始安装kubernetes-1.7.3】2.flannel、docker以及Harbor的配置以及作用
  • 【跃迁之路】【585天】程序员高效学习方法论探索系列(实验阶段342-2018.09.13)...
  • 【跃迁之路】【733天】程序员高效学习方法论探索系列(实验阶段490-2019.2.23)...
  • Druid 在有赞的实践
  • iOS小技巧之UIImagePickerController实现头像选择
  • leetcode46 Permutation 排列组合
  • socket.io+express实现聊天室的思考(三)
  • Twitter赢在开放,三年创造奇迹
  • use Google search engine
  • Vue全家桶实现一个Web App
  • 官方解决所有 npm 全局安装权限问题
  • 力扣(LeetCode)965
  • 聊一聊前端的监控
  • 吴恩达Deep Learning课程练习题参考答案——R语言版
  • 学习笔记:对象,原型和继承(1)
  •  一套莫尔斯电报听写、翻译系统
  • ​TypeScript都不会用,也敢说会前端?
  • ​业务双活的数据切换思路设计(下)
  • (06)Hive——正则表达式
  • (poj1.3.2)1791(构造法模拟)
  • (附源码)spring boot校园健康监测管理系统 毕业设计 151047
  • (四)docker:为mysql和java jar运行环境创建同一网络,容器互联
  • (转)ORM
  • (转载)OpenStack Hacker养成指南
  • (转载)从 Java 代码到 Java 堆
  • .NET MAUI学习笔记——2.构建第一个程序_初级篇
  • .NET Remoting Basic(10)-创建不同宿主的客户端与服务器端
  • .net wcf memory gates checking failed
  • .NET 材料检测系统崩溃分析
  • .NET6使用MiniExcel根据数据源横向导出头部标题及数据
  • .NET使用存储过程实现对数据库的增删改查
  • .NET中的Event与Delegates,从Publisher到Subscriber的衔接!
  • @CacheInvalidate(name = “xxx“, key = “#results.![a+b]“,multi = true)是什么意思
  • @DependsOn:解析 Spring 中的依赖关系之艺术
  • [2017][note]基于空间交叉相位调制的两个连续波在few layer铋Bi中的全光switch——
  • [51nod1610]路径计数
  • [ActionScript][AS3]小小笔记
  • [Android Studio 权威教程]断点调试和高级调试
  • [Android]Android P(9) WIFI学习笔记 - 扫描 (1)
  • [BZOJ] 2006: [NOI2010]超级钢琴