resource - xaml style wpf



WPF baml的bug:EventSetter在靜態資源被設置兩次,第二次為null (0)

如果我嘗試在xaml中存儲SetterBase對象的集合,那包括和EventSetter,則xaml加載器將引發錯誤。

根本原因是xaml加載器試圖設置PresentationFramework.dll!System.Windows.EventSetters.Event兩次:第一次到正確的值(ButtonBase.Click RoutedEvent),但第二次為null,並且這引發了一個異常。 我的附加屬性回調不涉及。

為什麼它嘗試將事件添加到EventSetter兩次,為什麼它是第二次null? 我檢查了正在使用的ctor是默認的,因此,EventSeetter不以任何不尋常的方式與集合交互,所以不是這樣。 實際的原因是wpf中的一個錯誤,它忽略了解析事件的兩部分結構(Event和EventHandler)的挑戰。

視圖

<Window x:Class="Spec.Plain.MTCMinimal"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ContentToggleButton;assembly=ContentToggleButton"
        Title="MTCMinimal" Height="300" Width="300">

<Window.Resources>

    <SetterBaseCollection x:Key="ButtonStyleSetters">
        <Setter Property="FrameworkElement.Height" Value="30"></Setter>
        <EventSetter Event="ButtonBase.Click" Handler="StyleClick" />
    </SetterBaseCollection>

</Window.Resources>

<Button Name="Button1"
        local:Behaviours.StyleSetters="{StaticResource ButtonStyleSetters}" />

後面的代碼只是InitializeComponent和事件處理程序的存根。 在InitializeComponent期間發生錯誤。

行為

public static readonly DependencyProperty StyleSettersProperty =
    DependencyProperty.RegisterAttached(
        "StyleSetters", typeof(MyStyleSetters),
        typeof(Behaviours),
        new PropertyMetadata(default(MyStyleSetters),
            ButtonSettersChanged));

private static void ButtonSettersChanged (DependencyObject d,
    DependencyPropertyChangedEventArgs args)
{
    var fe = d as FrameworkElement;
    if (fe == null) return;
    var ui = d as UIElement;

    var newValue = args.NewValue as MyStyleSetters;
    if (newValue != null)
    {
        foreach (var member in newValue)
        {
            var setter = member as Setter;
            if(setter != null)
            {
                fe.SetValue(setter.Property, setter.Value);
                continue;
            }
            var eventSetter = member as EventSetter;
            if (eventSetter == null) continue;
            if (ui == null || eventSetter.Event == null) continue;
            ui.AddHandler(eventSetter.Event, eventSetter.Handler);
        }
    }
}

public static void SetStyleSetters(DependencyObject element,
    MyStyleSetters value)
{
    element.SetValue(StyleSettersProperty, value);
}

public static MyStyleSetters GetStyleSetters (
    DependencyObject element)
{
    return (MyStyleSetters)element
        .GetValue(StyleSettersProperty);
}

錯誤

System.Windows.Markup.XamlParseException occurred
  _HResult=-2146233087
  _message='Set property 'System.Windows.EventSetter.Event' threw an exception.' Line number '11' and line position '26'.
  HResult=-2146233087
  IsTransient=false
  Message='Set property 'System.Windows.EventSetter.Event' threw an exception.' Line number '11' and line position '26'.
  Source=PresentationFramework
  LineNumber=11
  LinePosition=26
  StackTrace:
       at System.Windows.Markup.XamlReader.RewrapException(Exception e, IXamlLineInfo lineInfo, Uri baseUri)
  InnerException: System.ArgumentNullException
       _HResult=-2147467261
       _message=Value cannot be null.
       HResult=-2147467261
       IsTransient=false
       Message=Value cannot be null.
Parameter name: value
       Source=PresentationFramework
       ParamName=value
       StackTrace:
            at System.Windows.EventSetter.set_Event(RoutedEvent value)
       InnerException

調試

我在System.Windows.EventSetter.Event中設置了一個函數斷點,並記錄傳遞給setter的值。

然後我運行應用程序,並檢查輸出窗口,可以看到setter被擊中了兩次,第一次用正確的值,第二次的值為null ...

這個工作例子可以在GITHub Repo的解決方案中找到,名為EventSetterNull-SO-41604891-2670182

BAML

通過在XamlNodeList的Index成員中設置BP,我可以捕獲與SetterBaseCollection xaml對象關聯的xaml符號...

XamlNode [0] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [1] "StartObject: SetterBaseCollection"
XamlNode [2] "StartMember: _Items"
XamlNode [3] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [4] "StartObject: Setter"
XamlNode [5] "StartMember: Property"
XamlNode [6] "Value: Height"
XamlNode [7] "EndMember: "
XamlNode [8] "StartMember: Value"
XamlNode [9] "Value: 30"
XamlNode [10] "EndMember: "
XamlNode [11] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [12] "EndObject: "
XamlNode [13] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [14] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [15] "StartObject: EventSetter"
XamlNode [16] "StartMember: Event"
XamlNode [17] "Value: System.Windows.Baml2006.TypeConverterMarkupExtension"
XamlNode [18] "EndMember: "
               -->EventSetter value: {System.Windows.RoutedEvent}
XamlNode [19] "None: LineInfo: System.Xaml.LineInfo"
XamlNode [20] "StartMember: Event"
XamlNode [21] {System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Xaml.XamlNode.ToString() in ...\AppData\Local\JetBrains\Shared\v06\DecompilerCache\...\XamlNode.cs:line 159
   at <>x.<>m0(XamlNode& <>4__this)}
XamlNode [22] "EndMember: "
               -->EventSetter value: null
               !!!Then the null reference error throws
XamlNode [23] = "StartMember: Handler"
XamlNode [24] = "Value: StyleClick"
XamlNode [25] = "EndMember: "
XamlNode [26] = "None: LineInfo: System.Xaml.LineInfo"
XamlNode [27] = "EndObject: "
XamlNode [28] = "EndMember: "
XamlNode [29] = "EndObject: "
XamlNode [30] = "None: "
                 The remaining of the 41 nodes are all "None: "

錯誤?

baml nodeList看起來很奇怪,首先是從idx [20]開始的額外的Event成員,而這個成員實際上是System.NullReferenceException。
這被傳遞給XamlObjectWriter,然後傳遞給EventSetter屬性,這是錯誤的原因。
然後,baml按照預期繼續進行,顯示處理程序成員並正確地終止成員和對象。

結論

問題是在從XAML轉換到BAML,所以我會說這是一個錯誤。 雖然是一個可避免的邊緣情況。

變通

而不是嘗試在樣式中設置事件,請使用父對像中的附加屬性。 例如,一個StackPanel中的ButtonBase.Click =“StyleClick”將會把行為傳遞給我最初試圖做的所有事情。 屬性設置器的集合仍然可以在靜態資源中設置,並通過附加的基於屬性的行為來使用。

進一步研究根本原因

問題是一個事件屬性有兩個元素:事件和處理程序。 當Baml2006Reader解析baml中的一個對象時,需要考慮它的結構,以確保它處於正確的狀態以忠實地解釋對象成員。 要做到這一點,它有一個狀態機,從ReadObject中的while循環驅動,稱為Process_OneBamlRecord 。 此方法解碼下一個xamlNodeType並調用適當的方法來解析它並將其作為對象寫入。 其中一個方法叫做Process_Property ,它有一個特殊的邏輯硬連接到它來處理baml中的事件複合體。

問題是,如果事件作為Process_PropertyWithConverter記錄在baml中,則此方法不知道事件的特殊要求,並將所有事情填滿。 事件處理器的前綴是一個屬性標記(很可能事件解析器是為了遞歸的,並且對這個子結構使用相同的語法),並且由於沒有EndMember,StartMember狀態的變化,處理器的子屬性被解釋為ReadObject作為Event屬性。 而正在創建的事件setter對象會引發錯誤,因為它的Event屬性已經設置好了。