剛才開到智者千慮發的【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”