解包
编写WPF的时候,使用CallMethodAction去通过反射调用对应的方法的时候,遇到了一个问题,那就是,在TargetObject中是不能使用Binding去绑定对象的。
<Window.Resources>
<ObjectDataProvider x:Key="Ticks" ObjectType="{x:Type linq:Enumerable}" MethodName="Range">
<ObjectDataProvider.MethodParameters>
<sys:Int32>1</sys:Int32>
<sys:Int32>12</sys:Int32>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key="CurrentTime" ObjectType="{x:Type sys:DateTime}" MethodName="get_Now">
</ObjectDataProvider>
</Window.Resources>
<behaviors:Interaction.Triggers>
<behaviors:TimerTrigger MillisecondsPerTick="1000">
<behaviors:CallMethodAction MethodName="Rereash" TargetObject="{StaticResource CurrentTime}"/>
</behaviors:TimerTrigger>
</behaviors:Interaction.Triggers>
主要的思想涉及到解包,因为StaticResource给的是对象本身,但是Binding默认给的是包装后的数据上下文。
当我们使用{StaticResource}的时候,XAML解析器会去资源字典吧CurrentTime对象给返回给TargetObject。因此此时的TargetObject就是我们的——。反射引擎可以直接在他身上找到Refresh方法,
而我们使用{Binding},由于Binding是一个异步的有生命周期的观察者,因此TargetObject拿到的是Binding对象或者它还没有解析出来的Value。而CallMethodAction在初始化的时候就会去尝试匹配方法,因此就会反射失败,就会报错。
如果来深度解析一下!
CallMethodAction 的 TargetObject 是一个 DependencyProperty (依赖属性),而静态绑定在编译期间或者加载期间就会确定引用,是强类型的直接指向。
Binding 依赖于 DataContext。如果 TimerTrigger 所在的 UI 元素还没有完全挂载到逻辑树上,DataContext 可能是 null。
更加进一步的思考: 为啥不一开始就可以弄成反射消除的静态实现呢?
这主要因为Interaction.Triggers 和 CallMethodAction 并不是 WPF 核心框架的一部分,它们属于 Microsoft.Xaml.Behaviors 库(以前叫 Blend SDK)。
WPF的编译器MSBuild中的MarkupCompilePass负责处理核心标签(如 Grid, Button)。有一些黑盒行为,编译器在编译时只把它们当成普通的类。它并不知道 MethodName="Refresh" 里的字符串指向的是哪个具体的 C# 函数,因此只能留到运行时(Runtime)去用反射“找”这个方法。
还有一部分原因,C#是强类型的,但是传统XAML是基于字符串的,因此当你写 {Binding ...} 时,编译器并不知道这个 DataContext 到时候会是一个 ClockViewModel 还是一个 UserViewModel。
既然编译器不知道对象的“真身”,它就没法提前生成 CurrentTime.Refresh() 这样的硬编码调用,只能通过反射在运行时询问
扩展
在现代框架比如说Avalonia或者MAUI中,已经支持了消除反射的工作。也就是x:Bind,它在编译的时候会生成C#代码,如果你路径写错了,编译就会报错,而不是运行到一半才崩溃。原理就是显式指定x:DataType,让编译器去类里寻找。
方案:改用事件订阅(Event-driven)
不要让 XAML 去通过字符串找方法,直接在 C# 里把逻辑接通:
// 在 MainWindow.xaml.cs 中
public MainWindow() {
InitializeComponent();
// 定时器直接调用方法,完全不经过 XAML 反射
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
timer.Tick += (s, e) => (this.Resources["CurrentTime"] as MyClock)?.Refresh();
timer.Start();
}