解包

编写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();
}