刚才开到智者千虑发的【WPF】在Style中设置ToolTip的问题的博文,虽然最终给了一个暂时解决问题的方案,但是没有分析和解释其中的问题,正与他所说:但至于为什么不能直接在Setter.Value中放置TextBlock还是一个未解之谜。
趁着中午间隙,跟踪了一下,这里我将带给你完整的分析。
为了描述问题,首先,给出问题的xaml,当然,你也可以去智者千虑的blog查看详细描述。
<!--如下的写法没有问题-->
<!--<ToolTipService.ToolTip>
<TextBlock
Text="// 通过绑定等方式从某地方获取文本"
TextWrapping="Wrap"
Width="70" />
</ToolTipService.ToolTip>-->
<!--使用Style为ToolTip赋值,出错!将会抛出exception-->
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="ToolTipService.ToolTip">
<Setter.Value>
<TextBlock x:Name="tooltipBlock"
Text="// 通过绑定等方式从某地方获取文本"
TextWrapping="Wrap"
Width="70" />
</Setter.Value>
</Setter>
</Style>
</TextBlock.Style>
</TextBlock>
其中异常的信息为:

System.Windows.Markup.XamlParseException occurred
Message="Cannot add content of type 'System.Windows.Controls.TextBlock' to an object of type 'System.Object'. Error at object 'System.Windows.Controls.TextBlock' in markup file 'WpfApplication1;component/window1.xaml' Line 17 Position 30."
Source="PresentationFramework"
LineNumber=17
LinePosition=30
NameContext="Value"
StackTrace:
at System.Windows.Markup.XamlParseException.ThrowException(String message, Exception innerException, Int32 lineNumber, Int32 linePosition, Uri baseUri, XamlObjectIds currentXamlObjectIds, XamlObjectIds contextXamlObjectIds, Type objectType)
InnerException:
从异常信息来看,似乎是要将TextBlock设置为某个类型为Object的对象的Content,而对于WPF程序来说,外面开到的Exception是重新throw出来的,其实内部应该有另外的exception,所以这里需要打开IDE的Exceptions设置(通常快捷键为Ctrl+D,E),选中CLR Exception。然后再次调试,这次可以看到在上面的Exception之前,IDE捕捉到了另一个Exception:

通过这个Exception信息,可以知道是在BamlRecordReader的SetPropertyValueToParent方法里出现了问题,这里在尝试转换某个object对象成MemberInfo。那么这个SetPropertyToParent在做什么呢?从方法名称来看,是为当前节点的父节点的某个属性设值;那这里为什么又要把对象cast成MemberInfo呢?
要回答这些问题,我们得要去看看SetPropertyToParent的实现,在没有Microsoft Symbol Server的时候,最好的方式就是Reflector;当然如果能使用Symbol Server,那就可以直接设置断点,进入调试了。在我分析的时候,我是使用Reflector查看代码的,这个方法很长,但是里面总共只有5处会扔出exception,从每个exception的key来看这一段嫌疑很大:

由此结合Exception的描述分析,肯定是TextBlock在xaml的解析时,它的父节点是Object,这样,问题又来了,为什么呢?于是我们不得不回到我们写的xaml上,显然外面的TextBlock(名称为textBlockContainer)的肯定不会出问题,因为我们注释掉Style程序就正常了,问题肯定在Style。
我想学习过WPF之后都会知道XAML在解析过程是自顶向下,有外向内的解析的,在解析这个Style的时候,首先会创建一个Style对象,然后添加Setter,于是就解析到Setter了,也就要创建Setter对象,并为Setter对象的Property属性赋值为"ToolTipService.ToolTip";下面就解析到Setter的Value属性了,此时解析器需要创建对象TextBlock(名称为toolTipBlock),创建好了以后就把它设置到到父上的某个属性,通常是ContentProperty,如果没有就按照上面代码的顺序搜索,直到什么都没找到,扔个exception通知一下。这里TextBlock在Xaml中的父是谁?从XAML可以看到是“Setter.Value”,而这个Setter.Value在没有赋值的时候,取它返回的是一个DependencyProperty.UnsetValue,就是一个Object,显然,不可能为Object添加子,于是WPF系统认为异常。
结论:
至此,我们终于找到了问题根源,那就是在WPF的XAML节点的处理方式是实例化当前节点,然后将其赋值到它的父节点的某个属性,如果此时父节点是一个Object类型的属性时,就会出现exception。
解决方案
知道了为什么,下一步就会想到该如何解决。当然,智者千虑提供的方法是可行的,代码如下,这样就可以避过为TextBlock的父,即Setter.Value赋值了。

<TextBlock x:Name="textBlockContainer" Text="ABC" Margin="10">
<TextBlock.Resources>
<TextBlock x:Key="toolTipBlock"
Text="// 通过绑定等方式从某地方获取文本"
TextWrapping="Wrap"
Width="70" />
</TextBlock.Resources>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter
Property="ToolTipService.ToolTip"
Value="{StaticResource toolTipBlock}"/>
</Style>
</TextBlock.Style>
</TextBlock>