剛才開到智者千慮發的【WPF】在Style中設置ToolTip的問題的博文,雖然最終給了一個暫時解決問題的方案,但是沒有分析和解釋其中的問題,正與他所說:但至於為什麼不能直接在Setter.Value中放置TextBlock還是一個未解之謎。
趁著中午間隙,跟蹤了一下,這裡我將帶給你完整的分析。
為了描述問題,首先,給出問題的xaml,當然,你也可以去智者千慮的blog查看詳細描述。
<TextBlock x:Name="textBlockContainer" Text="ABC" Margin="10">
<!--如下的寫法沒有問題-->
<!--<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>
其中異常的信息為:
Exception
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
System.InvalidCastException occurred
Message="Unable to cast object of type 'System.Object' to type 'System.Reflection.MemberInfo'."
Source="PresentationFramework"
StackTrace:
at System.Windows.Markup.BamlRecordReader.SetPropertyValueToParent(Boolean fromStartTag, Boolean& isMarkupExtension)
InnerException:
通過這個Exception信息,可以知道是在BamlRecordReader的SetPropertyValueToParent方法裡出現了問題,這裡在嘗試轉換某個object對象成MemberInfo。那麼這個SetPropertyToParent在做什麼呢?從方法名稱來看,是為當前節點的父節點的某個屬性設值;那這裡為什麼又要把對象cast成MemberInfo呢?
要回答這些問題,我們得要去看看SetPropertyToParent的實現,在沒有Microsoft Symbol Server的時候,最好的方式就是Reflector;當然如果能使用Symbol Server,那就可以直接設置斷點,進入調試了。在我分析的時候,我是使用Reflector查看代碼的,這個方法很長,但是裡面總共只有5處會扔出exception,從每個exception的key來看這一段嫌疑很大:
element = this.GetCurrentObjectData();
//一段處理,和exception無關……
object parentObjectData = this.GetParentObjectData();
IDictionary dictionaryFromContext = this.GetDictionaryFromContext(parentContext, true);
if (dictionaryFromContext != null)
{
//這段貌似在處理字典資源……
}
else
{
IList listFromContext = this.GetListFromContext(parentContext);
if (listFromContext != null)
{
//parent是IList就調用IList把節點Add到父上……
}
else
{
ArrayExtension arrayExtensionFromContext = this.GetArrayExtensionFromContext(parentContext);
if (arrayExtensionFromContext != null)
{
//parent是數組……
}
else
{
IAddChild iAddChildFromContext = this.GetIAddChildFromContext(parentContext);
if (iAddChildFromContext != null)
{
//parent是IAddChild……
}
else
{
object contentProperty = parentContext.ContentProperty;
if (contentProperty != null)
{
//為某個property設值……
}
else if (parentContext.ContextType == ReaderFlags.PropertyComplexClr)
{
//為復雜clr屬性設值……
}
else if (parentContext.ContextType == ReaderFlags.PropertyComplexDP)
{
//為復雜DP設值……
}
else
{
//其他,貌似就扔異常了
Type parentType = this.GetParentType();
string parameter = (parentType == null) ? string.Empty : parentType.FullName;
if (element == null)
{
this.ThrowException("ParserCannotAddAnyChildren", parameter);
}
else
{
this.ThrowException("ParserCannotAddAnyChildren2", parameter, element.GetType().FullName);
}
}
}
}
}
}
}
由此結合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>
To be the apostrophe which changed “Impossible” into “I’m possible”